已合并
fix(npu-inductor): int32 index overflow — promote only overflow addends (variant C) #43736
fix(npu-inductor): int32 index overflow — promote only overflow addends (variant C) #43736
已合并
huyuchao创建于 8月4日
4 个文件变更+915-52
@@ -0,0 +1,440 @@
1+# Owner(s): ["module: tests"]
2+import functools
3+ 
4+import torch
5+import torch._dynamo as dynamo
6+from torch._inductor.utils import run_and_get_code
7+from torch.testing._internal.common_utils import (
8+ run_tests,
9+ instantiate_parametrized_tests,
10+)
11+from testutils import TestUtils
12+ 
13+import torch_npu # noqa: F401
14+import torch_npu._inductor.triton_experimental.config as ncfg
15+ 
16+ 
17+# Regression test for the int32 index-overflow bug in the triton_experimental
18+# NPU backend (2026-08).
19+#
20+# Background: for a reduction kernel whose total element count
21+# (numel * reduction_numel) exceeds int32_max, select_index_dtype() selects
22+# int64 indexing. Before the fix, that signal was propagated to every index
23+# construction (pid / arange tile / scalar odometer), which (a) let the
24+# overflowing pointer addend (e.g. 268435456*x1) wrap to a negative address in
25+# int32 → Ascend vector-core fault 507035, and (b) when the whole kernel was
26+# upcast to int64 instead, doubled UB on big arange tiles → vector-core timeout
27+# 507034. The fix keeps the big arange tiles int32 and widens EVERY axis
28+# factor in the address expressions at the load/store use site
29+# (coeff*var.to(tl.int64)) — unconditional, constructive correctness.
30+#
31+# This test pins the behavior at the 2^31 boundary: 8388609*256 == 2^31 + 257
32+# elements, just past int32_max, so the kernel must go int64-indexed without a
33+# full tile upcast.
34+def _free_hbm_bytes():
35+ try:
36+ free, _total = torch.npu.mem_get_info()
37+ return free
38+ except Exception:
39+ return None
40+ 
41+ 
42+def skipIfInsufficientHBM(min_free_bytes):
43+ """Skip a large test when the device's free HBM is verifiably below the
44+ requirement. The overflow corpus keeps the input, an eager reference,
45+ outputs and compile caches resident at once, so budget ~2x the tensor."""
46+ def deco(fn):
47+ @functools.wraps(fn)
48+ def wrapper(self):
49+ free = _free_hbm_bytes()
50+ if free is not None and free < min_free_bytes:
51+ self.skipTest(
52+ f"large test: needs >= {min_free_bytes >> 30} GiB free "
53+ f"HBM, only {free >> 30} GiB available"
54+ )
55+ return fn(self)
56+ return wrapper
57+ return deco
58+ 
59+ 
60+def _cut_autotune_runs():
61+ """Test-speedup: cut the mspti autotune benchmark to 1 warmup + 1 active run.
62+ 
63+ Guard-value note: the assertions of this suite are codegen-text and
64+ numeric-correctness checks, only ONE real kernel run is needed; the
65+ production 25-run-per-candidate bench ranks configs for performance,
66+ which the tests do not measure. The candidate set itself is left
67+ untouched (the with_index kernel at > 2^31 elements needs the
68+ production set: single-candidate pinning was tried and the largest
69+ tile fails UB compile while small-tile fallbacks produce wrong
70+ indices). Returns (old_warmup, old_active) for a try/finally restore.
71+ """
72+ old = (ncfg.mspti_warmup, ncfg.mspti_active)
73+ ncfg.mspti_warmup = 1
74+ ncfg.mspti_active = 1
75+ return old
76+ 
77+ 
78+# P0 drift pin (2026-08-28 audit): the audited call surface of the type-name
79+# helpers in the PINNED torch's codegen, keyed by source text without line
80+# numbers — a moved line does not false-alarm, an added/changed call site
81+# does. A torch bump failing test_int64_type_callsite_surface_pin means the
82+# int64 type-role assumptions need a re-audit (update this list only after
83+# re-auditing).
84+_EXPECTED_INT64_TYPE_CALLSURFACE = [
85+ "simd: return self.dtype_to_str(self.get_index_dtype_as_torch_dtype())",
86+ "triton: asm_triton_type = triton_type(dtype)",
87+ "triton: cast_inputs.append(f\"{inp}.to({triton_type(input_dtypes[i])})\")",
88+ "triton: f\"({str(logical_index)}).to({self.dtype_to_str(index_dtype)})\"",
89+ "triton: f\"({var}).to({triton_type(dtype)})\",",
90+ "triton: f\".to({triton_type(result_dtype)})\"",
91+ "triton: f\"{result} = {result}.to({triton_compute_type(target_dtype)})\"",
92+ "triton: f\"{torch.iinfo(index_dtype).max}, {self.dtype_to_str(index_dtype)})\"",
93+ "triton: f\"{value}.to({triton_compute_type(dtype)})\",",
94+ "triton: f\"{value}.to({triton_store_type(store_dtype)})\",",
95+ "triton: line += f\".to({triton_type(dtype)})\"",
96+ "triton: out = f\"{out}.to({triton_type(out_dtype)})\"",
97+ "triton: out = f\"{out}.to({triton_type(upcast_compute_type(dtype))})\"",
98+ "triton: out = f\"{x}.to({triton_type(dtype)}, bitcast=True)\"",
99+ "triton: out_dtype = triton_compute_type(dtype)",
100+ "triton: out_dtype = triton_store_type(dtype)",
101+ "triton: result = f\"{result}.to({self.dtype_to_str(result_type)})\"",
102+ "triton: result = f\"{result}.to({triton_type(result_dtype)})\"",
103+ "triton: return f\"({arg}).to({triton_type(dtype)})\"",
104+ "triton: return f\"{result}.to({triton_type(dtype)})\"",
105+ "triton: return f\"{result}.to({triton_type(result_dtype)})\"",
106+ "triton: return triton_compute_type(upcast_acc_dtype(dtype))",
107+ "triton: return triton_type(dtype)",
108+ "triton: return triton_type(upcast_compute_type(dtype))",
109+ "triton: triton_type = triton_compute_type(dtype)",
110+ "triton: value = f\"{value}.to({triton_store_type(store_dtype)})\"",
111+ "triton: x = f\"{x}.to({triton_type(src_dtype)})\"",
112+ "triton: {result_var}_ws = ({ws_name} + {self.index_to_str(ws_offset)}).to(tl.pointer_type({triton_type(dtype)}))",
113+]
114+ 
115+ 
116+class TestTritonExperimentalInt32Overflow(TestUtils):
117+ 
118+ @skipIfInsufficientHBM(17 * 2**30)
119+ def test_sum_over_int32_max_promotes_overflow_addend(self):
120+ # 2^31 + 257 elements (8.0 GiB fp32) — the minimal > int32_max case.
121+ x = torch.randn(8388609, 256, device=torch.device("npu"))
122+ ref = x.sum(dim=1)
123+ 
124+ def fn(t):
125+ return t.sum(dim=1)
126+ 
127+ cf = torch.compile(fn, options={"npu_backend": "triton_experimental"})
128+ y, codes = run_and_get_code(cf, x)
129+ 
130+ # Numeric correctness at > 2^31 scale.
131+ self.assertTrue(torch.allclose(y, ref, atol=1e-3, rtol=1e-3))
132+ # Every axis factor of the address expression is widened to int64 at
133+ # the load site (unconditional); lanes/tiles stay int32.
134+ self.assertIn("to(tl.int64)", codes[0])
R
Rrmch8月11日

这条断言对旧的全量 upcast 同样会通过,无法防住 507034。测试应确认 cast 只落在会溢出的具体地址 addend 上,同时反向禁止完整的 int64 arange/full;尤其应补 >2^31 argmax 和 codegen_linearize=False 的覆盖,确保不会重新生成满 int64 tile。

likedislike
huyuchao
24 天前 评论:
135+ 
136+ @skipIfInsufficientHBM(18 * 2**30)
137+ def test_dynamic_over_int32_max_unconditional_widen(self):
138+ # Dynamic axis length whose trace-time hint already exceeds int32_max.
139+ # The widening is unconditional (every axis factor in the address
140+ # expression), so there is no snapshot-derived promote decision and no
141+ # runtime guard contract: correctness holds for ANY runtime shape and
142+ # the hint boundary is irrelevant. This case used to exercise the
143+ # guard-skip path; it now pins that the dynamic >2^31 kernel simply
144+ # compiles and runs correctly.
145+ s0 = 2_150_000_000 # numel s0*2 = 4.3e9 > 2^31 (8.6 GiB fp16)
146+ x = torch.full((s0, 2), 1.0, device="npu", dtype=torch.float16)
147+ dynamo.mark_dynamic(x, 0)
148+ ref = x.sum(dim=1)
149+ 
150+ def fn(t):
151+ return t.sum(dim=1)
152+ 
153+ cf = torch.compile(fn, options={"npu_backend": "triton_experimental"})
154+ y = cf(x)
155+ torch.npu.synchronize()
156+ 
157+ self.assertTrue(torch.allclose(y, ref))
158+ 
159+ @skipIfInsufficientHBM(17 * 2**30)
160+ def test_non_linearize_over_int32_max_keeps_tiles_int32(self):
161+ # With codegen_linearize=False, past 2^31 elements the
162+ # non-linearize structure cannot stay correct — xoffset =
163+ # pid.to(int64)*XBLOCK upcasts every arange tile to int64 via mixed
164+ # broadcast (UB doubling, 507034) and the results go wrong. The kernel
165+ # must force the linearize structure (i64 rides the scalar
166+ # group_base/real_block chain and the widened address factors, tiles
167+ # stay int32) regardless of the config, which then only governs
168+ # in-range kernels.
169+ import torch_npu._inductor.triton_experimental.codegen.triton as tmod
170+ 
171+ orig = tmod.triton_codegen_linearize
172+ tmod.triton_codegen_linearize = False
173+ try:
174+ x = torch.full((8388609, 256), 1.0, device="npu", dtype=torch.float32)
175+ ref = x.sum(dim=1)
176+ 
177+ def fn(t):
178+ return t.sum(dim=1)
179+ 
180+ cf = torch.compile(fn, options={"npu_backend": "triton_experimental"})
181+ y, codes = run_and_get_code(cf, x)
182+ torch.npu.synchronize()
183+ 
184+ # Numeric correctness at > 2^31 scale with linearize forced off.
185+ self.assertTrue(torch.allclose(y, ref, atol=1e-3, rtol=1e-3))
186+ # Reverse-guard the whole-tile upcast: no arange/full tile may be
187+ # int64 AT ALL (as a dtype literal or via a cast). The pre-fix
188+ # all-tile upcast emitted tl.arange(..., tl.int64) with no ".to"
189+ # call, so asserting only on the cast would not catch it (507034).
190+ for line in codes[0].splitlines():
191+ if "tl.arange" in line or "tl.full" in line:
192+ self.assertNotIn(
193+ "tl.int64", line,
194+ f"non-linearize tile upcast past 2^31: {line.strip()}",
195+ )
196+ # Forward-guard: the overflowing addend — 256*x0
197+ # has term_max 256*8388608 == 2^31 > int32_max — must carry the
198+ # int64 cast. Widening is unconditional (constructive correctness),
199+ # so other axis factors of the same index may be cast too;
200+ # exclusivity is deliberately NOT pinned — a wrong widening
201+ # selection can only cost speed, never correctness.
202+ cast_lines = [l for l in codes[0].splitlines() if ".to(tl.int64)" in l]
203+ self.assertTrue(
204+ cast_lines,
205+ "no int64 cast in the address expressions at all",
206+ )
207+ self.assertRegex(
208+ "\n".join(cast_lines),
209+ r"\b256\*x0\.to\(tl\.int64\)",
210+ f"overflow addend cast missing: {cast_lines}",
211+ )
212+ finally:
213+ tmod.triton_codegen_linearize = orig
214+ 
215+ @skipIfInsufficientHBM(17 * 2**30)
216+ def test_max_with_index_over_int32_max_correct(self):
217+ # The upstream arg-reduction index accumulator follows
218+ # select_index_dtype() and is emitted as a full int64 tile past 2^31
219+ # elements (tl.full(..., tl.int64) + *_with_index compare/select over
220+ # the whole [X, R] tile). Variant C keeps this upstream default — the
221+ # tile works on NPU, verified correct — and promotes ONLY the pointer
222+ # addend to int64 at the load use site. Note aten.argmax itself does
223+ # NOT reach this path on NPU: it falls back to eager (no triton kernel
224+ # is generated), so torch.max(dim=1) is the reachable form of the
225+ # arg-reduction code path.
226+ #
227+ # Test time budget: the kernel is > 2^31 logical elements (hard
228+ # requirement of the overflow guard) and the with_index accumulator is
229+ # a full int64 tile, so a single real run costs ~3s on AIV; the
230+ # production autotune would then benchmark 20+ candidates x 25 runs
231+ # (~15min) without adding guard value. The assertions below need only
232+ # ONE real run (codegen texts + numeric correctness are
233+ # config-independent), so cut the mspti bench to 1+1 and leave the
234+ # candidate set untouched.
235+ orig_warmup, orig_active = _cut_autotune_runs()
236+ try:
237+ x = torch.randn(8388609, 256, device="npu")
238+ ref_v, ref_i = x.max(dim=1)
239+ 
240+ def fn(t):
241+ return t.max(dim=1)
242+ 
243+ cf = torch.compile(fn, options={"npu_backend": "triton_experimental"})
244+ result, codes = run_and_get_code(cf, x)
245+ yv, yi = result
246+ torch.npu.synchronize()
247+ 
248+ self.assertTrue(torch.allclose(yv, ref_v, atol=1e-3, rtol=1e-3))
249+ self.assertTrue(torch.equal(yi, ref_i))
250+ # The kernel must exist (compiled, not eager) and carry the overflow
251+ # addend cast (variant C at the load site).
252+ self.assertIn("with_index", codes[0])
253+ self.assertIn(".to(tl.int64)", codes[0])
254+ # R7-c trap pin: the with_index accumulator type and its
255+ # torch.iinfo(index_dtype).max sentinel fill must stay PAIRED —
256+ # narrowing dtype_to_str alone would emit the int64 max into a
257+ # tl.int32 full and break compilation.
258+ self.assertIn("9223372036854775807, tl.int64", codes[0])
259+ finally:
260+ ncfg.mspti_warmup = orig_warmup
261+ ncfg.mspti_active = orig_active
262+ 
263+ def test_mask_cmp_lhs_int64_narrow_protects_fp32(self):
264+ # With mask_cmp_fp32 on, the int32 narrow must
265+ # stay TERMINAL — falling through to the fp32 compare hangs the
266+ # kernel (triton-ascend lowers the fp32 mask compare into vector-core
267+ # work that never completes: 507034 vector-core timeout on a minimal
268+ # non-reduction kernel, >10 min no result on pointwise; verified
269+ # 2026-08-14). Pin the generated mask LHS texts: the default-off
270+ # layout byte-for-byte, and the fp32-on layout keeping the int64-narrow
271+ # case on the runnable int32 compare. These are pure-text assertions
272+ # (no compile) so they stay green while the fp32 path is broken.
273+ import torch_npu._inductor.triton_experimental.codegen.npu_header as nh
274+ import sympy
275+ 
276+ off_cases = [
277+ # (label, index_expr, numel, index_dtype, expected_lhs)
278+ ("int64-narrow", "x0index", 256, "tl.int64", "(x0index).to(tl.int32)"),
279+ ("int64-at-boundary", "x0index", 2**31, "tl.int64", "x0index"),
280+ ("int64-dynamic", "x0index", sympy.Symbol("s"), "tl.int64", "x0index"),
281+ ("int32", "x0index", 256, "tl.int32", "x0index"),
282+ ]
283+ orig = nh.npu_mask_cmp_fp32
284+ try:
285+ nh.npu_mask_cmp_fp32 = False
286+ for label, e, n, d, want in off_cases:
287+ self.assertEqual(
288+ nh._mask_cmp_lhs(e, n, d), want,
289+ f"mask_cmp_fp32 off: {label}",
290+ )
291+ # fp32 on: the int64 narrow keeps the int32 compare (protection);
292+ # the in-range int32 case is unchanged (the pre-fix fp32 cast).
293+ nh.npu_mask_cmp_fp32 = True
294+ self.assertEqual(
295+ nh._mask_cmp_lhs("x0index", 256, "tl.int64"),
296+ "(x0index).to(tl.int32)",
297+ "int64 narrow must not fall through to the hanging fp32 compare",
298+ )
299+ self.assertEqual(
300+ nh._mask_cmp_lhs("x0index", 256, "tl.int32"),
301+ "(x0index).to(tl.float32)",
302+ )
303+ finally:
304+ nh.npu_mask_cmp_fp32 = orig
305+ 
306+ def test_non_linearize_in_range_reduction_grid_exact(self):
307+ # In-range non-linearize reduction: the launcher grid must be exactly
308+ # ceil(xnumel/XBLOCK). The heuristics' grid_0 defaults to the persistent
309+ # NPU_CU_COUNT (48) for reduction kernels, which is only valid under the
310+ # linearize structure's group dispatch — a non-linearize kernel
311+ # (xoffset = pid*XBLOCK, always-true xmask, no group folding) over-reads
312+ # past the input when 48 > ceil (MTE fault 507035, the pre-fix failure)
313+ # and silently drops tiles when 48 < ceil (uninitialized output rows, the
314+ # min(ceil, 48) clamp regression). Codegen flags
315+ # inductor_meta["npu_linearize"]=False; the heuristics then launches the
316+ # exact tile count instead of the fixed 48.
317+ import torch_npu._inductor.triton_experimental.codegen.triton as tmod
318+ 
319+ orig = tmod.triton_codegen_linearize
320+ tmod.triton_codegen_linearize = False
321+ try:
322+ x = torch.randn(64, 128, 256, device="npu")
323+ ref = x.sum(dim=-1)
324+ 
325+ def fn(t):
326+ return t.sum(dim=-1)
327+ 
328+ cf = torch.compile(fn, options={"npu_backend": "triton_experimental"})
329+ y, codes = run_and_get_code(cf, x)
330+ torch.npu.synchronize()
331+ 
332+ # Numeric correctness pins both failure modes: the 507035 over-read
333+ # (pre-fix) and the missing-tile clamp regression (wrong results).
334+ self.assertTrue(torch.allclose(y, ref, atol=1e-3, rtol=1e-3))
335+ # Classic non-linearize structure, in-range so no int64 promotion.
336+ self.assertIn("xoffset = tl.program_id(0) * XBLOCK", codes[0])
337+ self.assertNotIn("tl.int64", codes[0])
338+ finally:
339+ tmod.triton_codegen_linearize = orig
340+ 
341+ 
342+ 
343+ def test_expand_over_int32_blocks_odometer_i64(self):
344+ # Oversized-block-count dispatch audit: a static numel >= 2^31 must
345+ # never become
346+ # a triton literal. Three poison paths found and fixed by the
347+ # expand->sum >2^31 probe: (1) codegen_static_numels stomping the i64
348+ # runtime arg with a bare literal (uint32 typing in [2^31, 2^32) ->
349+ # signedness errors in the div/mod dispatch chains), (2) per-axis
350+ # tl.constexpr numels in the linearize header, (3) the r-tree
351+ # tt.equal_to constants specialization (uint32->i64 vcast rejected by
352+ # BiShengIR). All three keep the numel on its i64 runtime arg, so the
353+ # whole block-dispatch scalar chain promotes to int64. This case has
354+ # > 2^31 total blocks via a stride-0 expand axis (numel 2.2e9,
355+ # storage ONE element) — cheap, no HBM skip needed.
356+ n = 2_200_000_000 # > 2^31, inside the uint32 window [2^31, 2^32)
357+ x = torch.ones((1,), device="npu").expand((n,))
358+ ref = x.sum()
359+ 
360+ def fn(t):
361+ return t.sum()
362+ 
363+ cf = torch.compile(fn, options={"npu_backend": "triton_experimental"})
364+ y, codes = run_and_get_code(cf, x)
365+ torch.npu.synchronize()
366+ self.assertTrue(torch.allclose(y, ref, rtol=1e-5))
367+ # no bare >= 2^31 numel literal in any generated kernel (stomp or
368+ # constexpr forms)
369+ for code in codes:
370+ self.assertNotRegex(code, r"numel = 2\d{9}")
371+ self.assertNotRegex(code, r"numel : tl.constexpr = 2\d{9}")
372+ 
373+ def test_dtype_role_isolation_pins(self):
374+ # P0 pin: the dtype-role separation is currently BY CONVENTION (three
375+ # mutually-counteracting global patches; see NPUTritonKernel.
376+ # dtype_to_str's SCOPE comment for the audited truth). These pins turn
377+ # silent drift (upstream sync, future edits) into loud failures.
378+ import torch._inductor.utils as inductor_utils
379+ 
380+ import torch_npu._inductor.triton_experimental.codegen.triton as te_triton
381+ 
382+ # (a) in-range kernel stays byte-clean int32: the widening must not
383+ # leak into kernels that do not need it
384+ x = torch.randn(1024, 64, device="npu")
385+ 
386+ cf = torch.compile(lambda t: t.sum(1), options={"npu_backend": "triton_experimental"})
387+ y, codes = run_and_get_code(cf, x)
388+ self.assertTrue(torch.allclose(y, x.sum(1), atol=1e-3, rtol=1e-3))
389+ self.assertNotIn("tl.int64", codes[0])
390+ 
391+ # (b) compute types are NOT demoted: npu_triton_compute_type has no
392+ # int64 branch and bypasses the demotion mapping
393+ self.assertEqual(te_triton.npu_triton_compute_type(torch.int64), "tl.int64")
394+ 
395+ # (c) seam policy pins: the mapping itself still demotes bare
396+ # triton_type calls (kept for default-backend coexistence), but the
397+ # two former seams are explicitly OPEN — int64 stores stay tl.int64
398+ # (npu_triton_store_type) and non-int64 dtypes delegate upstream.
399+ # Any change in either direction must be a reviewed decision.
400+ # Requires activation (the compile above installs the patches).
401+ import torch._inductor.codegen.triton as up_codegen
402+ 
403+ self.assertEqual(inductor_utils.triton_type(torch.int64), "tl.int32")
404+ self.assertEqual(up_codegen.triton_store_type(torch.int64), "tl.int64")
405+ self.assertEqual(up_codegen.triton_store_type(torch.bool), "tl.int8")
406+ self.assertEqual(
407+ up_codegen.triton_store_type(torch.float32), "tl.float32"
408+ )
409+ 
410+ def test_int64_type_callsite_surface_pin(self):
411+ # P0 pin: upstream drift detector, paired with
412+ # _EXPECTED_INT64_TYPE_CALLSURFACE above. Failing on a torch bump is
413+ # BY DESIGN: re-audit the int64 type-role surface, then update the
414+ # list.
415+ import inspect
416+ import re
417+ 
418+ import torch._inductor.codegen.simd as up_simd
419+ import torch._inductor.codegen.triton as up_triton
420+ 
421+ pat = re.compile(r"(dtype_to_str|triton_type|triton_compute_type|triton_store_type)\(")
422+ surface = set()
423+ for mod in (up_triton, up_simd):
424+ short = mod.__name__.rsplit(".", 1)[-1]
425+ for line in open(inspect.getsourcefile(mod)).read().splitlines():
426+ s = line.strip()
427+ if pat.search(s) and not s.startswith(("def ", "#")):
428+ surface.add(f"{short}: {s}")
429+ self.assertEqual(
430+ sorted(surface),
431+ sorted(_EXPECTED_INT64_TYPE_CALLSURFACE),
432+ "int64 type-helper call surface drifted — re-audit the dtype "
433+ "roles before updating this snapshot",
434+ )
435+ 
436+ 
437+instantiate_parametrized_tests(TestTritonExperimentalInt32Overflow)
438+ 
439+if __name__ == "__main__":
440+ run_tests()
@@ -23,13 +23,69 @@ from torch._inductor.codegen.triton import texpr
23npu_mask_cmp_fp32 = ncfg.mask_cmp_fp3223npu_mask_cmp_fp32 = ncfg.mask_cmp_fp32
24 24 
25 25 
26-def _mask_cmp_lhs(index_expr: str) -> str:26+def _mask_cmp_lhs(index_expr: str, numel=None, index_dtype: str = "tl.int32") -> str:
27- """Wrap a mask LHS so the `<` runs on the vector unit (fp32) when possible."""27+ """Wrap a mask LHS so the `<` runs on the vector unit when possible.
28+ 
29+ int64 index vectors (kernels whose total element count exceeds 2^31) have
30+ no native int64 vector compare on Ascend, so a raw int64 ``<`` decays to a
31+ scalar loop. Per-axis indices stay below 2^31 -- only the final linear
32+ address combine needs int64 -- so cast the LHS back to int32 for the
33+ compare: exact below 2^31 and keeps the compare on the vector unit. The
34+ int32 cast is skipped when the axis itself could reach 2^31 (a wrapping
35+ mask would be silently wrong) or its length is dynamic (numel not a static
36+ int); those keep the correct (scalarized) int64 compare.
37+ 
38+ The early return is deliberate PROTECTION, not an oversight: with
39+ mask_cmp_fp32 on, falling through to the fp32 compare
40+ hangs the kernel -- triton-ascend lowers the fp32 mask compare into
41+ vector-core work that never completes (507034 vector-core timeout on a
42+ minimal non-reduction kernel, and >10 min with no result on pointwise;
43+ verified 2026-08-14). The int32 narrow must therefore stay terminal; the
44+ fp32 path should be re-enabled only after the triton-ascend lowering is
45+ fixed, at which point the fall-through can be restored per the review.
46+ """
47+ if index_dtype == "tl.int64" and isinstance(numel, (int, sympy.Integer)) and int(numel) < 2**31:
R
Rrmch8月11日

这里仅在 mask_cmp_fp32=True 时有问题:变体 C 的 linearize header 已让 arange/scalar odometer 保持 int32,所以该分支实际是 int32→int32 空转,并提前 return 掉后面的 fp32 compare 路径。默认关闭该配置时没有行为变化;开启时会丢失原有向量比较优化。建议先完成安全的 int32 收窄,再继续应用 mask_cmp_fp32,并补配置开启时的生成源码断言。

likedislike
huyuchao
24 天前 评论:
48+ return f"({index_expr}).to(tl.int32)"
28 if not npu_mask_cmp_fp32:49 if not npu_mask_cmp_fp32:
29 return index_expr50 return index_expr
30 return f"({index_expr}).to(tl.float32)"51 return f"({index_expr}).to(tl.float32)"
31 52 
32 53 
54+def _npu_emit_axis_numel(pre_loop, tree, node):
55+ """Emit the per-axis numel definition (oversized-block-count dispatch audit). Static values < 2^31 fold to
56+ a tl.constexpr (upstream behaviour). A static value >= 2^31 must NOT
57+ become a triton literal — values in [2^31, 2^32) type as uint32 and
58+ poison the div/mod dispatch chains with signedness errors, and larger
59+ values blow int32 block counts — so it aliases the tree's i64 runtime
60+ arg (guaranteed i64: an axis >= 2^31 forces total numel >= 2^31, which
61+ selects index_dtype=int64 and size_dtype types the arg i64). The alias
62+ divides out the sibling axes' product, exact by range-tree
63+ construction (tree numel == product of free-axis numels)."""
64+ length = node.length
65+ if not isinstance(length, (int, sympy.Integer)):
66+ return
67+ if int(length) < 2**31:
68+ pre_loop.writeline(f"{node.name}numel : tl.constexpr = {int(length)}")
69+ return
70+ sib_prod = sympy.Integer(1)
71+ for n in tree.nodes.values():
72+ if n.name in tree.tree_node_mapping or n.name == node.name:
73+ continue
74+ sib_prod = sib_prod * sympy.sympify(n.length)
75+ if not (sib_prod.is_Integer and int(sib_prod) >= 1):
76+ raise RuntimeError(
77+ f"[triton_experimental] axis {node.name!r} has a static numel "
78+ f">= 2^31 ({length}) with non-static sibling axes; this "
79+ "combination needs a dedicated lowering (oversized-block-count dispatch)"
80+ )
81+ if int(sib_prod) == 1:
82+ pre_loop.writeline(f"{node.name}numel = {tree.prefix}numel")
83+ else:
84+ pre_loop.writeline(
85+ f"{node.name}numel = {tree.prefix}numel // {int(sib_prod)}"
86+ )
87+ 
88+ 
33def _ordered_mapping_items(mapping):89def _ordered_mapping_items(mapping):
34 """Yield (name, expr) entries in dependency order: any name referenced by90 """Yield (name, expr) entries in dependency order: any name referenced by
35 another mapping's expression is emitted before that mapping. The dict's91 another mapping's expression is emitted before that mapping. The dict's
@@ -470,14 +526,12 @@ def _codegen_header_npu_for_tree(kernel, tree, code, outer_blocks=None):
470 # Scalar odometer axis: no register tile. real_block==1 so the odometer526 # Scalar odometer axis: no register tile. real_block==1 so the odometer
471 # walks one element/block (block count == numel) and the index is a scalar.527 # walks one element/block (block count == numel) and the index is a scalar.
472 if node.name in _scalar_odo_names:528 if node.name in _scalar_odo_names:
473- if isinstance(node.length, (int, sympy.Integer)):529+ _npu_emit_axis_numel(pre_loop, tree, node)
474- pre_loop.writeline(f"{node.name}numel : tl.constexpr = {int(node.length)}")
475 pre_loop.writeline(f"real_block_{node.name} : tl.constexpr = 1")530 pre_loop.writeline(f"real_block_{node.name} : tl.constexpr = 1")
476 continue531 continue
477 # Unify candidates get real_block emitted after the tile is aligned below.532 # Unify candidates get real_block emitted after the tile is aligned below.
478 if node.name in _unify_names:533 if node.name in _unify_names:
479- if isinstance(node.length, (int, sympy.Integer)):534+ _npu_emit_axis_numel(pre_loop, tree, node)
480- pre_loop.writeline(f"{node.name}numel : tl.constexpr = {int(node.length)}")
481 continue535 continue
482 # Under greedy-via-unify every non-scalar-odo free axis is a unify536 # Under greedy-via-unify every non-scalar-odo free axis is a unify
483 # candidate; the only other path was the legacy real_block=numel//divisor537 # candidate; the only other path was the legacy real_block=numel//divisor
@@ -658,6 +712,20 @@ def _codegen_header_npu_for_tree(kernel, tree, code, outer_blocks=None):
658 # offsets, emitted after real_block. odometer_opt (default ON): B1 hoists the712 # offsets, emitted after real_block. odometer_opt (default ON): B1 hoists the
659 # cumulative block product and divides pid once (shorter div chain); B2 drops713 # cumulative block product and divides pid once (shorter div chain); B2 drops
660 # provably single-block axes (static numel==1 -> offset 0, no cumprod term).714 # provably single-block axes (static numel==1 -> offset 0, no cumprod term).
715+ # Oversized-block-count dispatch audit — static numels >= 2^31 must
716+ # never become triton
717+ # literals. The original "safe by promotion" audit was REFUTED by the
718+ # expand->sum >2^31 probe (2026-08-29): literals in [2^31, 2^32) type as
719+ # uint32 (signedness errors in the div/mod dispatch chains) and equal_to
720+ # specialization turns the uint32 into a BiShengIR-rejected uint32->i64
721+ # vcast. Fixed at every emission point (codegen_static_numels, the
722+ # per-axis constexpr numels via _npu_emit_axis_numel, block counts, and
723+ # the r-tree constants specialization): such numels stay on their i64
724+ # runtime arg (size_dtype=index_dtype in int64 mode), so the block
725+ # counts, cumblk products, (group_base + i) and % blocks promote to i64
726+ # end-to-end through triton's own rules. Pinned by
727+ # test_expand_over_int32_blocks_odometer_i64 (> 2^31 total blocks via a
728+ # stride-0 expand axis — cheap, 1-element storage).
661 _odo_opt = ncfg.odometer_opt729 _odo_opt = ncfg.odometer_opt
662 _free_nodes_ordered = [n for n in tree.nodes.values()730 _free_nodes_ordered = [n for n in tree.nodes.values()
663 if n.name not in tree.tree_node_mapping]731 if n.name not in tree.tree_node_mapping]
@@ -669,7 +737,16 @@ def _codegen_header_npu_for_tree(kernel, tree, code, outer_blocks=None):
669 for node in _free_nodes_ordered:737 for node in _free_nodes_ordered:
670 divisor_is_static = isinstance(node.divisor, (int, sympy.Integer))738 divisor_is_static = isinstance(node.divisor, (int, sympy.Integer))
671 length_is_static = isinstance(node.length, (int, sympy.Integer))739 length_is_static = isinstance(node.length, (int, sympy.Integer))
672- blocks_is_constexpr = length_is_static and divisor_is_static740+ # Oversized block counts: a static numel >= 2^31 is aliased to the i64 runtime arg by
741+ # _npu_emit_axis_numel, so its block count must be runtime too.
742+ blocks_is_constexpr = (
743+ length_is_static
744+ and divisor_is_static
745+ and not (
746+ isinstance(node.length, (int, sympy.Integer))
747+ and int(node.length) >= 2**31
748+ )
749+ )
673 if blocks_is_constexpr:750 if blocks_is_constexpr:
674 pre_loop.writeline(f"{node.name}_blocks : tl.constexpr = ({node.name}numel + real_block_{node.name} - 1) // real_block_{node.name}") # noqa: B950751 pre_loop.writeline(f"{node.name}_blocks : tl.constexpr = ({node.name}numel + real_block_{node.name} - 1) // real_block_{node.name}") # noqa: B950
675 else:752 else:
@@ -708,6 +785,11 @@ def _codegen_header_npu_for_tree(kernel, tree, code, outer_blocks=None):
708 tree.node_block_constexpr = {}785 tree.node_block_constexpr = {}
709 tree.node_block_constexpr.update(node_arange_upper)786 tree.node_block_constexpr.update(node_arange_upper)
710 787 
788+ # Kernels whose total element count exceeds 2^31 index with tl.int64 (the
789+ # dtype_to_str override in triton.py restores index_dtype). Arange tiles stay
790+ # int32 (variant C): index_to_str in triton.py promotes only the overflow
791+ # addend (e.g. 268435456*x1) to int64 at the load/store use site, keeping the
792+ # big vector tiles int32 (all-int64 tiles double UB and hang -- 507034).
711 for node in tree.nodes.values():793 for node in tree.nodes.values():
712 if node.name in tree.tree_node_mapping:794 if node.name in tree.tree_node_mapping:
713 continue795 continue
@@ -738,7 +820,8 @@ def _codegen_header_npu_for_tree(kernel, tree, code, outer_blocks=None):
738 line = kernel.iteration_ranges_scalar_code(tree, f"{node.name}offset")820 line = kernel.iteration_ranges_scalar_code(tree, f"{node.name}offset")
739 header_code.writeline(f"{node.name}index = {line}")821 header_code.writeline(f"{node.name}index = {line}")
740 header_code.writeline(f"{node.name} = {node.name}index")822 header_code.writeline(f"{node.name} = {node.name}index")
741- header_code.writeline(f"{node.name}mask = {_mask_cmp_lhs(f'{node.name}index')} < {node.name}numel")823+ mask_lhs = _mask_cmp_lhs(f"{node.name}index", node.length, kernel.index_dtype)
824+ header_code.writeline(f"{node.name}mask = {mask_lhs} < {node.name}numel")
742 825 
743 # Every free axis is now tiled flat (greedy-via-unify / scalar-odometer /826 # Every free axis is now tiled flat (greedy-via-unify / scalar-odometer /
744 # static-constexpr): needs_inner_loop is False for all of them, so no axis827 # static-constexpr): needs_inner_loop is False for all of them, so no axis
@@ -755,6 +755,87 @@ _TritonPrinter._print_Float = _npu_print_Float
755_TritonPrinter._print_ToFloat = _npu_print_ToFloat755_TritonPrinter._print_ToFloat = _npu_print_ToFloat
756 756 
757 757 
758+# Variant C int64 widening, structural (replaces the old re.sub text rewrite).
759+# NpuWiden(x) is an unevaluated marker whose ONLY meaning is printer-level: it
760+# renders as "x.to(tl.int64)" (same trick as ToFloat above). The cast is
761+# attached to axis factors inside pointer-arithmetic expressions at the sympy
762+# level (NPUTritonKernel.index_to_str), so no rendered-text matching is
763+# involved and composite terms (c*x0*x1, FloorDiv(x, k)*c) widen correctly.
764+class NpuWiden(sympy.Function):
765+ @classmethod
766+ def eval(cls, arg):
767+ return None # stay unevaluated; survives Add/Mul rebalancing
768+ 
769+ 
770+def _npu_print_NpuWiden(self, expr):
771+ # Parenthesize anything that is not an atom (same idiom as ToFloat); for
772+ # the plain monomial case the atom renders bare, byte-identical to the
773+ # old regex output ("268435456*x1.to(tl.int64)").
774+ from sympy.printing.precedence import PRECEDENCE
775+ s = self.parenthesize(expr.args[0], PRECEDENCE["Atom"] - 0.5)
776+ return f"{s}.to(tl.int64)"
777+ 
778+ 
779+_TritonPrinter._print_NpuWiden = _npu_print_NpuWiden
780+ 
781+ 
782+# R3 hardening: the numel-level gate (select_index_dtype ->
783+# can_use_32bit_indexing) only checks total numel and buffer storage sizes,
784+# so address expressions carrying large index-arithmetic constants (upstream
785+# #186057: model integer math, unfold/flatten offset algebra — constants NOT
786+# reflected in any buffer's storage) can overflow int32 in a kernel the gate
787+# typed as int32. Widening is cheap (perf-waived) and shape-independent, so
788+# arm it on any static proximity hint instead of trying to prove overflow:
789+# an integer literal of magnitude >= 2^30 anywhere in the expression
790+# (addend, stride coefficient or mod/div base) can bust 2^31 once combined
791+# with an axis extent of the same order; a STATIC axis within a whisker of
792+# int32_max busts with even a small constant addend. Ordinary strides and
793+# offsets sit far below both thresholds.
794+_NPU_INT32_ARM_LITERAL = 2**30
795+ 
796+ 
797+def _npu_should_widen_address(index_dtype: str, expr: sympy.Expr, range_tree_nodes) -> bool:
798+ """Whether index_to_str widens this address expression's axis factors:
799+ int64-typed kernels always; int32-typed kernels only on an #186057-class
800+ hint (conservative arm — a false positive costs the waived i64 address
801+ tax, never correctness).
802+ 
803+ Two hints: (a) any integer literal >= 2^30 (fast path); (b) an
804+ expression-level bound — substitute every axis symbol with its max lane
805+ value and evaluate against the trace-time hint (the upstream
806+ #186057/d630a2d direction, vendored as a boolean gate). Arm when the
807+ WHOLE expression can bust int32, which closes the dynamic
808+ near-max-axis + small-constant corner the literal/axis heuristics
809+ missed. Non-monotone terms (ModularIndexing) make a point substitution
810+ unsound as a maximum, so their presence arms unconditionally; no hint
811+ (unbacked) or any evaluation failure arms too — every tie breaks toward
812+ the fail-safe direction. A different runtime shape recompiles through
813+ the ordinary shape guards with a fresh hint, so the decision is
814+ re-evaluated per instance."""
815+ if index_dtype == "tl.int64":
816+ return True
817+ if any(abs(int(lit)) >= _NPU_INT32_ARM_LITERAL for lit in expr.atoms(sympy.Integer)):
818+ return True
819+ if expr.has(ModularIndexing):
820+ return True
821+ subs = {}
822+ for sym in expr.free_symbols:
823+ node = range_tree_nodes.get(sym)
824+ if node is not None:
825+ subs[sym] = node.length - 1
826+ if not subs:
827+ return False
828+ try:
829+ bound_expr = expr.subs(subs)
830+ if isinstance(bound_expr, (int, sympy.Integer)):
831+ bound = int(bound_expr)
832+ else:
833+ bound = int(V.graph.sizevars.guarding_hint_or_throw(bound_expr))
834+ return abs(bound) > 2**31 - 1
835+ except Exception:
836+ return True
837+ 
838+ 
758# Tensor-dimension symbol kinds: any tensor actually indexed by a running kernel839# Tensor-dimension symbol kinds: any tensor actually indexed by a running kernel
759# has every dim >= 1, so an expression built only from these is >= 1.840# has every dim >= 1, so an expression built only from these is >= 1.
760_DIM_SYMT = (SymT.SIZE, SymT.PRECOMPUTED_SIZE, SymT.UNBACKED_INT)841_DIM_SYMT = (SymT.SIZE, SymT.PRECOMPUTED_SIZE, SymT.UNBACKED_INT)
@@ -982,6 +1063,69 @@ def npu_triton_compute_type(dtype):
982torch._inductor.codegen.triton.triton_compute_type = npu_triton_compute_type1063torch._inductor.codegen.triton.triton_compute_type = npu_triton_compute_type
983 1064 
984 1065 
1066+# P1-1 / R7 seams (audited 2026-08-29): int64 must not be silently demoted on
1067+# the two routes that bypass NPUTritonKernel.dtype_to_str — stores of int64
1068+# tensors (value-correct until now only while stored values fit int32) and
1069+# index-as-value casts (which actively NARROWED int64 values back to tl.int32
1070+# in int32-mode kernels). Both are re-opened with the same module-patch
1071+# pattern as npu_triton_compute_type above; like it, they are process-global
1072+# and thus shared with the default backend in mixed processes (accepted,
1073+# consistent precedent).
1074+from torch._inductor.codegen.triton import (
1075+ TritonSymbols as _TritonSymbols,
1076+ triton_store_type as _upstream_triton_store_type,
1077+ triton_type as _upstream_triton_type,
1078+)
1079+ 
1080+ 
1081+def npu_triton_store_type(dtype):
1082+ """int64 stores stay tl.int64 (upstream routes through the demotion
1083+ mapping); every other dtype delegates to upstream unchanged."""
1084+ if dtype == torch.int64:
1085+ return "tl.int64"
1086+ return _upstream_triton_store_type(dtype)
1087+ 
1088+ 
1089+torch._inductor.codegen.triton.triton_store_type = npu_triton_store_type
1090+ 
1091+ 
1092+def _npu_value_type_str(dtype):
1093+ if dtype == torch.int64:
1094+ return "tl.int64"
1095+ return _upstream_triton_type(dtype)
1096+ 
1097+ 
1098+@classmethod
1099+def _npu_value_expr(cls, expr, dtype):
1100+ """Patched copy of upstream TritonSymbols.value_expr, kept in sync with
1101+ it; the ONLY change is the final cast routing through
1102+ _npu_value_type_str so int64 index values are not narrowed back to
1103+ tl.int32 by the demotion mapping.
1104+ 
1105+ Like :meth:`index_expr`, but honors ``dtype`` by setting the kernel
1106+ index dtype before emitting, and casting the result if needed.
1107+ """
1108+ real_index_dtype = V.kernel._index_dtype
1109+ V.kernel._index_dtype = (
1110+ dtype if dtype in (torch.int32, torch.int64) else torch.int64
1111+ )
1112+ try:
1113+ var = cls.index_expr(expr, dtype)
1114+ finally:
1115+ V.kernel._index_dtype = real_index_dtype
1116+ if real_index_dtype != dtype or var.dtype != dtype:
1117+ var = V.kernel.cse.generate(
1118+ V.kernel.compute,
1119+ f"({var}).to({_npu_value_type_str(dtype)})",
1120+ dtype=dtype,
1121+ shape=var.shape,
1122+ )
1123+ return var
1124+ 
1125+ 
1126+_TritonSymbols.value_expr = _npu_value_expr
1127+ 
1128+ 
985class _FlatMapExpr:1129class _FlatMapExpr:
986 """Render-only adapter for collapse_rowmajor_xtree: quacks like a sympy expr1130 """Render-only adapter for collapse_rowmajor_xtree: quacks like a sympy expr
987 (``free_symbols`` + ``str()``) but emits plain ``(F // d) % L`` the printer can1131 (``free_symbols`` + ``str()``) but emits plain ``(F // d) % L`` the printer can
@@ -1521,6 +1665,69 @@ class NPUTritonKernelOverrides(TritonKernelOverrides):
1521class NPUTritonKernel(TritonKernel):1665class NPUTritonKernel(TritonKernel):
1522 overrides = NPUTritonKernelOverrides # type: ignore[assignment]1666 overrides = NPUTritonKernelOverrides # type: ignore[assignment]
1523 1667 
1668+ def dtype_to_str(self, dtype):
1669+ # The apply_npu_codegen_patches demotion (_triton_type_mapping
1670+ # ["tl.int64"] -> "tl.int32") forces index_dtype to "tl.int32" for every
1671+ # kernel, silently wrapping pointer offsets past 2^31 (e.g.
1672+ # 268435456*x1 for x1 >= 8 in a >2^31-element reduction) into negative
1673+ # addresses that fault on Ascend (507035 / vector core exception).
1674+ # Returning the upstream-native "tl.int64" here restores that signal.
1675+ #
1676+ # SCOPE (audited 2026-08-28; do not trust the old "only the index
1677+ # path is affected" claim): this override reaches EVERY
1678+ # dtype_to_str(torch.int64) call site — the index_dtype derivation
1679+ # (simd.py index_dtype property), the arg-reduction with_index trio
1680+ # (accumulator type AND its torch.iinfo(index_dtype).max sentinel,
1681+ # which MUST stay type-consistent — narrowing one side alone emits
1682+ # tl.full(size, 2**63-1, tl.int32) and breaks compilation), and the
1683+ # reduction intermediate-result cast (upstream
1684+ # reduction_collapse_dims) which is a DATA context. npu_triton_compute_type
1685+ # likewise has no int64->int32 branch and bypasses the mapping, so
1686+ # compute types are not demoted either. As of 2026-08-29 the backend
1687+ # is fully int64-open across ALL routes: the two former seams —
1688+ # value_expr (index-as-value casts) and triton_store_type (int64
1689+ # tensor stores) — are explicitly re-opened via module patches next
1690+ # to npu_triton_compute_type (they previously stayed demoted through
1691+ # the mapping, correct only while stored values fit int32). The
1692+ # remaining demotion surface is the mapping's int64 entry itself,
1693+ # vestigial for this backend (dtype_to_str overridden, patched routes
1694+ # bypass it) and kept for default-backend coexistence in mixed
1695+ # processes; any NEW direct triton_type(torch.int64) call site is
1696+ # caught by the callsite surface pin in
1697+ # test_triton_experimental_int32_overflow.
1698+ if dtype == torch.int64:
R
Rrmch8月11日

上游 arg reduction 的 index accumulator 也会走 dtype_to_str(index_dtype)triton.py:4947–4949)。元素数超过 2^31 时,这个 override 会生成完整的 tl.full(..., tl.int64) accumulator tile;index_to_str() 对地址 addend 的局部提升覆盖不到这里,codegen_linearize=False 时 arange/full 也会沿上游路径按 self.index_dtype 全量 i64。地址计算所需的 i64 应与 reduction 数据/index accumulator dtype 解耦,并补 >2^31 argmax 和 non-linearize 回归测试。

likedislike
huyuchao
24 天前 评论:
1699+ return "tl.int64"
1700+ return super().dtype_to_str(dtype)
1701+ 
1702+ def codegen_static_numels(self, code):
1703+ # Oversized-block-count dispatch audit: upstream stomps static
1704+ # numels with bare
1705+ # literals ("r0_numel = 2200000000"). A literal in [2^31, 2^32) types
1706+ # as triton uint32 and poisons every downstream scalar chain with
1707+ # signedness errors (rsplit dispatch //, group-base %, found by the
1708+ # expand->sum >2^31 probe), and block counts past 2^31 wrap int32.
1709+ # Keep such numels on their runtime arg — typed i64 via
1710+ # size_dtype=index_dtype in int64 mode — so the whole dispatch chain
1711+ # promotes to int64 instead. Otherwise kept in sync with upstream
1712+ # TritonKernel.codegen_static_numels; the persistent-reduction branch
1713+ # raises because NPU disables persistent reductions.
1714+ for tree in self.range_trees:
1715+ if not tree.is_reduction or self.inside_reduction:
1716+ simplified_tree_numel = V.graph.sizevars.simplify(tree.numel)
1717+ if (
1718+ isinstance(simplified_tree_numel, (sympy.Integer, int))
1719+ and int(simplified_tree_numel) < 2**31
1720+ ):
1721+ code.writeline(f"{tree.prefix}numel = {int(simplified_tree_numel)}")
1722+ if tree.is_reduction and self.persistent_reduction:
1723+ raise AssertionError(
1724+ "persistent reduction is disabled on NPU "
1725+ "(should_use_persistent_reduction -> False); extend this "
1726+ "override from upstream if that ever changes"
1727+ )
1728+ if tree.prefix == "x" and self.no_x_dim:
1729+ code.writeline("XBLOCK: tl.constexpr = 1")
1730+ 
1524 def should_use_persistent_reduction(self) -> bool:1731 def should_use_persistent_reduction(self) -> bool:
1525 # NPU does not support persistent reduction; always use the looped path1732 # NPU does not support persistent reduction; always use the looped path
1526 # so that the accumulator (tl.full) is properly initialized.1733 # so that the accumulator (tl.full) is properly initialized.
@@ -1790,7 +1997,7 @@ class NPUTritonKernel(TritonKernel):
1790 """1997 """
1791 if not ncfg.select_extract_slice:1998 if not ncfg.select_extract_slice:
1792 return1999 return
1793- if not triton_codegen_linearize:2000+ if not self._npu_linearize:
1794 return2001 return
1795 if self.inside_reduction:2002 if self.inside_reduction:
1796 return2003 return
@@ -1837,9 +2044,60 @@ class NPUTritonKernel(TritonKernel):
1837 # needs to prove dim >= 1. Mirror the upstream list path.2044 # needs to prove dim >= 1. Mirror the upstream list path.
1838 if isinstance(index, list):2045 if isinstance(index, list):
1839 return f"[{', '.join(map(self.index_to_str, index))}]"2046 return f"[{', '.join(map(self.index_to_str, index))}]"
1840- return self.kexpr(self.rename_indexing(_drop_size_clamp(index)))2047+ expr = self.rename_indexing(_drop_size_clamp(index))
2048+ if _npu_should_widen_address(self.index_dtype, expr, self.range_tree_nodes):
2049+ # Constructive correctness: widen EVERY axis factor in the address
2050+ # expression, unconditionally — no term analysis, no bounds, no
2051+ # runtime guards. Every product/sum then computes in int64 (triton
2052+ # promotes on any i64 operand), so no address can wrap regardless
2053+ # of term shape (composite strides, FloorDiv/Mod, negative
2054+ # monomials) and correctness holds for ANY runtime shape: there is
2055+ # no snapshot to distrust and nothing to recompile. Tile headers
2056+ # keep lanes int32 (linearize bypasses the upstream
2057+ # arange/full/pid casts); the i64 footprint is bounded by
2058+ # (#free axes x XBLOCK) per address site and absorbed by the
2059+ # autotuner's small-tile fallback when a kernel sits near the UB
2060+ # ceiling. Dropping provably-redundant casts is deliberately NOT
2061+ # this layer's job: a wrong selection may only cost speed, never
2062+ # correctness, so it belongs to an optional demotion pass on top
2063+ # (upstream #91028/d630a2d ValueRange semantics), never here.
2064+ for axis in expr.free_symbols & set(self.range_tree_nodes):
2065+ expr = expr.subs(axis, NpuWiden(axis))
2066+ return self.kexpr(expr)
1841 2067 
1842 def __init__(self, *args, **kwargs):2068 def __init__(self, *args, **kwargs):
2069+ # Placeholder before super().__init__() — initialize_range_tree() and
2070+ # the pre-codegen header pass (both run inside super().__init__) gate
2071+ # on this flag. The cross-boundary test MUST match select_index_dtype
2072+ # exactly (same features instance, cached): past 2^31 elements the
2073+ # non-linearize structure cannot stay correct (xoffset =
2074+ # pid.to(i64)*XBLOCK upcasts every arange tile via mixed broadcast —
2075+ # UB doubling 507034 and wrong results), so force the
2076+ # linearize structure: the i64 rides the scalar group_base/real_block
2077+ # chain, tiles stay int32. codegen_linearize then only governs
2078+ # in-range kernels.
2079+ features = kwargs.get("features")
2080+ if features is None and len(args) > 1:
2081+ features = args[1]
2082+ force_linearize = False
2083+ if features is not None:
2084+ try:
2085+ force_linearize = (
2086+ self.dtype_to_str(features.select_index_dtype()) == "tl.int64"
2087+ )
2088+ except Exception:
2089+ # Fail SAFE, not silent: linearize is the structure this
2090+ # backend deems correct past 2^31, so an undecidable kernel
2091+ # must not fall back to the non-linearize path on a swallowed
2092+ # exception (config semantics may not silently revert to the
2093+ # unsafe path). An in-range kernel hitting this only pays the
2094+ # linearize structure — never correctness.
2095+ log.warning(
2096+ "select_index_dtype raised; forcing linearize (safe side)",
2097+ exc_info=True,
2098+ )
2099+ force_linearize = True
2100+ self._npu_linearize: bool = bool(triton_codegen_linearize) or force_linearize
1843 super().__init__(*args, **kwargs)2101 super().__init__(*args, **kwargs)
1844 self._axis_split_subs: Dict[sympy.Symbol, sympy.Expr] = {}2102 self._axis_split_subs: Dict[sympy.Symbol, sympy.Expr] = {}
1845 # r-axis cross-core split (OUTER reduction): when True, codegen_kernel2103 # r-axis cross-core split (OUTER reduction): when True, codegen_kernel
@@ -1859,7 +2117,7 @@ class NPUTritonKernel(TritonKernel):
1859 # detection replaces the old regex text classification.2117 # detection replaces the old regex text classification.
1860 self._npu_select_lane_loads: Dict[str, Dict[str, Any]] = {}2118 self._npu_select_lane_loads: Dict[str, Dict[str, Any]] = {}
1861 2119 
1862- if triton_codegen_linearize:2120+ if self._npu_linearize:
1863 for tree in self.range_trees:2121 for tree in self.range_trees:
1864 if not hasattr(tree, 'tree_node_mapping'):2122 if not hasattr(tree, 'tree_node_mapping'):
1865 tree.tree_node_mapping = {}2123 tree.tree_node_mapping = {}
@@ -1943,7 +2201,7 @@ class NPUTritonKernel(TritonKernel):
1943 2201 
1944 def prepare_indexing(self, index):2202 def prepare_indexing(self, index):
1945 index = super().prepare_indexing(index)2203 index = super().prepare_indexing(index)
1946- if triton_codegen_linearize:2204+ if self._npu_linearize:
1947 index = self._maybe_split_fused_axes(index)2205 index = self._maybe_split_fused_axes(index)
1948 index = self._maybe_split_strided_axis(index)2206 index = self._maybe_split_strided_axis(index)
1949 index = self._simplify_compound_indexing(index)2207 index = self._simplify_compound_indexing(index)
@@ -2923,7 +3181,7 @@ class NPUTritonKernel(TritonKernel):
2923 def initialize_range_tree(self, pid_cache):3181 def initialize_range_tree(self, pid_cache):
2924 """Override to add tree_node_mapping for linearize mode."""3182 """Override to add tree_node_mapping for linearize mode."""
2925 super().initialize_range_tree(pid_cache)3183 super().initialize_range_tree(pid_cache)
2926- if triton_codegen_linearize:3184+ if self._npu_linearize:
2927 for tree in self.range_trees:3185 for tree in self.range_trees:
2928 if not hasattr(tree, 'tree_node_mapping'):3186 if not hasattr(tree, 'tree_node_mapping'):
2929 tree.tree_node_mapping = {}3187 tree.tree_node_mapping = {}
@@ -2958,7 +3216,7 @@ class NPUTritonKernel(TritonKernel):
2958 return imports.getvalue()3216 return imports.getvalue()
2959 3217 
2960 def triton_tensor_ndim(self):3218 def triton_tensor_ndim(self):
2961- if triton_codegen_linearize and getattr(self, '_linearize_applied', False):3219+ if self._npu_linearize and getattr(self, '_linearize_applied', False):
2962 ndim = 03220 ndim = 0
2963 for tree in self.range_trees:3221 for tree in self.range_trees:
2964 if tree.tensor_dim is not None:3222 if tree.tensor_dim is not None:
@@ -2981,7 +3239,7 @@ class NPUTritonKernel(TritonKernel):
2981 return sum(int(tree.tensor_dim is not None) for tree in self.range_trees)3239 return sum(int(tree.tensor_dim is not None) for tree in self.range_trees)
2982 3240 
2983 def dense_size_str(self):3241 def dense_size_str(self):
2984- if triton_codegen_linearize:3242+ if self._npu_linearize:
2985 ndim = self.triton_tensor_ndim()3243 ndim = self.triton_tensor_ndim()
2986 if ndim == 0:3244 if ndim == 0:
2987 return "[]"3245 return "[]"
@@ -2996,7 +3254,7 @@ class NPUTritonKernel(TritonKernel):
2996 return f"[{', '.join(sizes)}]"3254 return f"[{', '.join(sizes)}]"
2997 3255 
2998 def reduction_resize(self, value):3256 def reduction_resize(self, value):
2999- if triton_codegen_linearize:3257+ if self._npu_linearize:
3000 if not self.no_x_dim and self.inside_reduction:3258 if not self.no_x_dim and self.inside_reduction:
3001 ndims = self.triton_tensor_ndim()3259 ndims = self.triton_tensor_ndim()
3002 if ndims <= 1:3260 if ndims <= 1:
@@ -3031,7 +3289,7 @@ class NPUTritonKernel(TritonKernel):
3031 # covers sum → upstream emits "[:, None]", broadcasting the OUTER store to [XBLOCK,3289 # covers sum → upstream emits "[:, None]", broadcasting the OUTER store to [XBLOCK,
3032 # XBLOCK] (~100x). Mirror the permuted-slot logic, deviating ONLY in permuted-linearize.3290 # XBLOCK] (~100x). Mirror the permuted-slot logic, deviating ONLY in permuted-linearize.
3033 if (3291 if (
3034- triton_codegen_linearize3292+ self._npu_linearize
3035 and not self.no_x_dim3293 and not self.no_x_dim
3036 and self.inside_reduction3294 and self.inside_reduction
3037 and getattr(self, "_npu_tile_permuted", False)3295 and getattr(self, "_npu_tile_permuted", False)
@@ -3058,7 +3316,7 @@ class NPUTritonKernel(TritonKernel):
3058 # arange slice and mask. Upstream _combine_contiguous_dims() merges [x0(100),3316 # arange slice and mask. Upstream _combine_contiguous_dims() merges [x0(100),
3059 # x1(4)] into one flat x2(400), which has no per-node shape and breaks the3317 # x1(4)] into one flat x2(400), which has no per-node shape and breaks the
3060 # broadcast. Disable entirely in linearize mode.3318 # broadcast. Disable entirely in linearize mode.
3061- if triton_codegen_linearize:3319+ if self._npu_linearize:
3062 return index3320 return index
3063 return super()._combine_contiguous_dims(index, tree)3321 return super()._combine_contiguous_dims(index, tree)
3064 3322 
@@ -3075,7 +3333,7 @@ class NPUTritonKernel(TritonKernel):
3075 self._npu_prepared_load_index = (3333 self._npu_prepared_load_index = (
3076 result.index if isinstance(result, IndexingOptions) else None3334 result.index if isinstance(result, IndexingOptions) else None
3077 )3335 )
3078- if not triton_codegen_linearize:3336+ if not self._npu_linearize:
3079 return result3337 return result
3080 3338 
3081 # Linearize mode emits per-node masks (x0mask, x1mask, …) and xmask = their AND,3339 # Linearize mode emits per-node masks (x0mask, x1mask, …) and xmask = their AND,
@@ -3218,7 +3476,7 @@ class NPUTritonKernel(TritonKernel):
3218 )3476 )
3219 3477 
3220 def iteration_ranges_get_pid(self, entry: IterationRangesRoot) -> str:3478 def iteration_ranges_get_pid(self, entry: IterationRangesRoot) -> str:
3221- if not triton_codegen_linearize:3479+ if not self._npu_linearize:
3222 return super().iteration_ranges_get_pid(entry)3480 return super().iteration_ranges_get_pid(entry)
3223 3481 
3224 assert entry.grid_dim is not None3482 assert entry.grid_dim is not None
@@ -3226,8 +3484,6 @@ class NPUTritonKernel(TritonKernel):
3226 # all dimensions — yz grid overflow handling is not needed here.3484 # all dimensions — yz grid overflow handling is not needed here.
3227 key = "(group_base + i)"3485 key = "(group_base + i)"
3228 pid = entry.pid_cache.get(key, key)3486 pid = entry.pid_cache.get(key, key)
3229- if self.index_dtype != "tl.int32":
3230- return f"{pid}.to({self.index_dtype})"
3231 return pid3487 return pid
3232 3488 
3233 def codegen_range_tree(self):3489 def codegen_range_tree(self):
@@ -3236,7 +3492,7 @@ class NPUTritonKernel(TritonKernel):
3236 For non-r dimensions, calls our custom codegen_header_npu instead of the3492 For non-r dimensions, calls our custom codegen_header_npu instead of the
3237 default iteration_ranges_codegen_header.3493 default iteration_ranges_codegen_header.
3238 """3494 """
3239- if not triton_codegen_linearize:3495+ if not self._npu_linearize:
3240 return super().codegen_range_tree()3496 return super().codegen_range_tree()
3241 3497 
3242 # npu_header owns the large linearize header generator; import lazily to3498 # npu_header owns the large linearize header generator; import lazily to
@@ -3276,9 +3532,9 @@ class NPUTritonKernel(TritonKernel):
3276 total_ndim = max(orig_ndim, npu_ndim, tree.tensor_dim + 1)3532 total_ndim = max(orig_ndim, npu_ndim, tree.tensor_dim + 1)
3277 sizes = ["None"] * total_ndim3533 sizes = ["None"] * total_ndim
3278 sizes[tree.tensor_dim] = ":"3534 sizes[tree.tensor_dim] = ":"
3279- index_dtype = self.index_dtype3535+ # Arange tiles stay int32 (variant C): the pointer arithmetic
3280- suffix = f".to({index_dtype})" if index_dtype != "tl.int32" else ""3536+ # promotes only the overflow addend to int64 at the use site.
3281- ranges_code = f"tl.arange(0, {tree.prefix.upper()}BLOCK)[{', '.join(sizes)}]{suffix}"3537+ ranges_code = f"tl.arange(0, {tree.prefix.upper()}BLOCK)[{', '.join(sizes)}]"
3282 self.body.writeline(3538 self.body.writeline(
3283 f"{tree.prefix}base = {ranges_code}"3539 f"{tree.prefix}base = {ranges_code}"
3284 )3540 )
@@ -3299,7 +3555,7 @@ class NPUTritonKernel(TritonKernel):
3299 assignments (x0 = x0index) are emitted by _codegen_header_npu_for_tree.3555 assignments (x0 = x0index) are emitted by _codegen_header_npu_for_tree.
3300 Only r-tree (loop) entries use the default body/indexing_code path.3556 Only r-tree (loop) entries use the default body/indexing_code path.
3301 """3557 """
3302- if not triton_codegen_linearize:3558+ if not self._npu_linearize:
3303 return super().codegen_iteration_ranges_entry(entry)3559 return super().codegen_iteration_ranges_entry(entry)
3304 3560 
3305 if not entry.root.is_reduction:3561 if not entry.root.is_reduction:
@@ -3344,7 +3600,7 @@ class NPUTritonKernel(TritonKernel):
3344 transpose of the in-loop ``r0_base`` (the latter built from tensor_dim) —3600 transpose of the in-loop ``r0_base`` (the latter built from tensor_dim) —
3345 a loop-carried-type conflict when a free X-axis takes the inner-loop path.3601 a loop-carried-type conflict when a free X-axis takes the inner-loop path.
3346 """3602 """
3347- if not triton_codegen_linearize:3603+ if not self._npu_linearize:
3348 return super().iteration_ranges_ranges_code(entry)3604 return super().iteration_ranges_ranges_code(entry)
3349 3605 
3350 assert entry.tensor_dim is not None3606 assert entry.tensor_dim is not None
@@ -3370,23 +3626,23 @@ class NPUTritonKernel(TritonKernel):
3370 else:3626 else:
3371 effective_dim += 13627 effective_dim += 1
3372 size = self.indexing_size_str(effective_dim)3628 size = self.indexing_size_str(effective_dim)
3373- index_dtype = self.index_dtype3629+ # Arange tiles stay int32 (variant C); see codegen_range_tree.
3374- convert = f".to({index_dtype})" if index_dtype != "tl.int32" else ""3630+ return f"tl.arange(0, {entry.prefix.upper()}BLOCK){size}"
3375- return f"tl.arange(0, {entry.prefix.upper()}BLOCK){size}{convert}"
3376 3631 
3377 def iteration_ranges_scalar_code(self, entry: IterationRangesRoot, value) -> str:3632 def iteration_ranges_scalar_code(self, entry: IterationRangesRoot, value) -> str:
3378 """3633 """
3379 Linearize mode: override scalar_code.3634 Linearize mode: override scalar_code.
3380 """3635 """
3381- if not triton_codegen_linearize:3636+ if not self._npu_linearize:
3382 return super().iteration_ranges_scalar_code(entry, value)3637 return super().iteration_ranges_scalar_code(entry, value)
3383 3638 
3384- index_dtype = self.index_dtype3639+ # Scalar odometer offsets stay int32 (variant C): the int64 cast for
3640+ # overflow addends rides on the scalar/latent tile at the use site.
3385 ndim = self.triton_tensor_ndim()3641 ndim = self.triton_tensor_ndim()
3386 size = [1] * ndim3642 size = [1] * ndim
3387 if self.no_x_dim:3643 if self.no_x_dim:
3388- return f"tl.full([1, 1], {value}, {index_dtype})"3644+ return f"tl.full([1, 1], {value}, tl.int32)"
3389- return f"tl.full({size}, {value}, {index_dtype})"3645+ return f"tl.full({size}, {value}, tl.int32)"
3390 3646 
3391 def _npu_rsplit_rprefix(self) -> str:3647 def _npu_rsplit_rprefix(self) -> str:
3392 """Reduction-tree prefix (e.g. 'r0_') for the rsplit partial kernel."""3648 """Reduction-tree prefix (e.g. 'r0_') for the rsplit partial kernel."""
@@ -3661,7 +3917,7 @@ class NPUTritonKernel(TritonKernel):
3661 # Collect block hints for linearize mode3917 # Collect block hints for linearize mode
3662 block_hints = {}3918 block_hints = {}
3663 axis_hints = []3919 axis_hints = []
3664- if triton_codegen_linearize:3920+ if self._npu_linearize:
3665 for tree in self.range_trees:3921 for tree in self.range_trees:
3666 if tree.prefix != 'r':3922 if tree.prefix != 'r':
3667 block_hints[f"{tree.prefix.upper()}BLOCK_HINT"] = tree.get_block_hint()3923 block_hints[f"{tree.prefix.upper()}BLOCK_HINT"] = tree.get_block_hint()
@@ -3733,7 +3989,7 @@ class NPUTritonKernel(TritonKernel):
3733 # is no longer emitted as a runtime arg: the greedy-via-unify tile3989 # is no longer emitted as a runtime arg: the greedy-via-unify tile
3734 # scheme computes real_block from XBLOCK + size hints and never3990 # scheme computes real_block from XBLOCK + size hints and never
3735 # references <node>divisor in the body, so the arg was dead.)3991 # references <node>divisor in the body, so the arg was dead.)
3736- if tree.prefix != 'r' and triton_codegen_linearize:3992+ if tree.prefix != 'r' and self._npu_linearize:
3737 tree_node_mapping = getattr(tree, 'tree_node_mapping', {})3993 tree_node_mapping = getattr(tree, 'tree_node_mapping', {})
3738 for node in tree.nodes.values():3994 for node in tree.nodes.values():
3739 if node.name in tree_node_mapping:3995 if node.name in tree_node_mapping:
@@ -3769,13 +4025,13 @@ class NPUTritonKernel(TritonKernel):
3769 "mix_mode": "aiv", # NPU: force vector kernel generation4025 "mix_mode": "aiv", # NPU: force vector kernel generation
3770 }4026 }
3771 triton_meta["configs"] = [config_of(signature)]4027 triton_meta["configs"] = [config_of(signature)]
3772- if triton_codegen_linearize:4028+ if self._npu_linearize:
3773 triton_meta['block_hints'] = block_hints4029 triton_meta['block_hints'] = block_hints
3774 triton_meta['axis_hints'] = axis_hints4030 triton_meta['axis_hints'] = axis_hints
3775 4031 
3776 optimize_mem = V.graph.is_inference or V.graph.is_backward4032 optimize_mem = V.graph.is_inference or V.graph.is_backward
3777 npu_num_x_nodes = 04033 npu_num_x_nodes = 0
3778- if triton_codegen_linearize:4034+ if self._npu_linearize:
3779 for tree in self.range_trees:4035 for tree in self.range_trees:
3780 if not tree.is_reduction and not tree.is_loop:4036 if not tree.is_reduction and not tree.is_loop:
3781 tree_node_mapping = getattr(tree, 'tree_node_mapping', {})4037 tree_node_mapping = getattr(tree, 'tree_node_mapping', {})
@@ -3812,12 +4068,19 @@ class NPUTritonKernel(TritonKernel):
3812 # Mark static r-tree numel as constants so NPU compiler can prove4068 # Mark static r-tree numel as constants so NPU compiler can prove
3813 # r0_mask = (r0_index < r0_numel) is always true when R0_BLOCK == r0_numel,4069 # r0_mask = (r0_index < r0_numel) is always true when R0_BLOCK == r0_numel,
3814 # eliminating the scalar select/boundary-check path in the ttadapter.4070 # eliminating the scalar select/boundary-check path in the ttadapter.
3815- if triton_codegen_linearize:4071+ if self._npu_linearize:
3816 for tree in self.range_trees:4072 for tree in self.range_trees:
3817 if tree.is_reduction and isinstance(tree.numel, (int, sympy.Integer)):4073 if tree.is_reduction and isinstance(tree.numel, (int, sympy.Integer)):
3818 numel_name = f"{tree.prefix}numel"4074 numel_name = f"{tree.prefix}numel"
3819 if any(getattr(s, 'name', None) == numel_name for s in signature):4075 if any(getattr(s, 'name', None) == numel_name for s in signature):
3820- triton_meta["constants"][numel_name] = int(tree.numel)4076+ # Oversized block counts: a constant >= 2^31 specializes the i64 arg to a
4077+ # value triton types as uint32 (in [2^31, 2^32)), and
4078+ # the resulting uint32->i64 vcast is rejected by
4079+ # BiShengIR. Keep such numels on their i64 runtime
4080+ # arg; the mask-elimination this enables is
4081+ # irrelevant at >2^31 reduction sizes anyway.
4082+ if int(tree.numel) < 2**31:
4083+ triton_meta["constants"][numel_name] = int(tree.numel)
3821 4084 
3822 self.triton_meta = triton_meta4085 self.triton_meta = triton_meta
3823 4086 
@@ -3839,11 +4102,22 @@ class NPUTritonKernel(TritonKernel):
3839 # codegen_body() has populated pre_loop_code, so the recipe reads finished4102 # codegen_body() has populated pre_loop_code, so the recipe reads finished
3840 # block-count lines. A5 only (other chips keep group dispatch). Must run before the4103 # block-count lines. A5 only (other chips keep group dispatch). Must run before the
3841 # heuristics decorator serializes inductor_meta.4104 # heuristics decorator serializes inductor_meta.
3842- if triton_codegen_linearize and device_props.is_a5():4105+ if self._npu_linearize and device_props.is_a5():
3843 _recipe = self._npu_build_grid_recipe()4106 _recipe = self._npu_build_grid_recipe()
3844 if _recipe is not None:4107 if _recipe is not None:
3845 inductor_meta["npu_dispatch_recipe"] = _recipe4108 inductor_meta["npu_dispatch_recipe"] = _recipe
3846 4109 
4110+ # Linearize flag for the launcher grid: the heuristics grid_0 defaults to
4111+ # NPU_CU_COUNT (persistent 48-core dispatch) for reduction kernels, which
4112+ # is only valid under the linearize structure's group logic. A
4113+ # non-linearize kernel emits xoffset = pid*XBLOCK with an always-true
4114+ # xmask (xnumel % XBLOCK == 0) and one tile per pid, so the grid must be
4115+ # exactly ceil(xnumel/XBLOCK): larger reads past the input (MTE fault
4116+ # 507035, verified at XBLOCK=512/grid=48 on a 8192-xnumel sum), smaller
4117+ # silently drops tiles. The heuristics launches the exact tile count
4118+ # when this flag is False.
4119+ inductor_meta["npu_linearize"] = self._npu_linearize
4120+ 
3847 for helper in self.helper_functions:4121 for helper in self.helper_functions:
3848 code.writeline("")4122 code.writeline("")
3849 code.splice(helper)4123 code.splice(helper)
@@ -3886,7 +4160,7 @@ class NPUTritonKernel(TritonKernel):
3886 self.codegen_static_numels(code)4160 self.codegen_static_numels(code)
3887 for old, new in self.args.aliases():4161 for old, new in self.args.aliases():
3888 code.writeline(f"{old} = {new}")4162 code.writeline(f"{old} = {new}")
3889- if triton_codegen_linearize:4163+ if self._npu_linearize:
3890 # Emit the intra-core block->core dispatch prologue (pre_loop4164 # Emit the intra-core block->core dispatch prologue (pre_loop
3891 # hoists, total_blocks, group_size/group_base), then the body in4165 # hoists, total_blocks, group_size/group_base), then the body in
3892 # the per-core "for i" loop.4166 # the per-core "for i" loop.
@@ -4330,7 +4604,7 @@ class NPUTritonKernel(TritonKernel):
4330 return4604 return
4331 if self.inside_reduction:4605 if self.inside_reduction:
4332 return4606 return
4333- if not (triton_codegen_linearize and getattr(self, "_linearize_applied", False)):4607+ if not (self._npu_linearize and getattr(self, "_linearize_applied", False)):
4334 return4608 return
4335 targets = getattr(self, "_npu_select_lane_loads", None)4609 targets = getattr(self, "_npu_select_lane_loads", None)
4336 if not targets:4610 if not targets:
@@ -4555,7 +4829,7 @@ class NPUTritonKernel(TritonKernel):
4555 numel/divisor args in linearize mode.4829 numel/divisor args in linearize mode.
4556 In 2.7.1 the method is add_numel_to_call_args (no _and_grid suffix).4830 In 2.7.1 the method is add_numel_to_call_args (no _and_grid suffix).
4557 """4831 """
4558- if not triton_codegen_linearize:4832+ if not self._npu_linearize:
4559 # Non-linearize: upstream behavior4833 # Non-linearize: upstream behavior
4560 for tree in self.range_trees:4834 for tree in self.range_trees:
4561 expr = tree.numel if isinstance(tree.numel, (sympy.Integer, sympy.Symbol)) else V.graph.wrapper_code.generate_numel_expr(name, tree) # noqa: B9504835 expr = tree.numel if isinstance(tree.numel, (sympy.Integer, sympy.Symbol)) else V.graph.wrapper_code.generate_numel_expr(name, tree) # noqa: B950
@@ -4970,7 +5244,7 @@ class NPUTritonScheduling(TritonScheduling):
4970 for kernel in kernels:5244 for kernel in kernels:
4971 self.codegen_node_schedule_with_kernel(node_schedule, kernel)5245 self.codegen_node_schedule_with_kernel(node_schedule, kernel)
4972 5246 
4973- if triton_codegen_linearize:5247+ if kernels and kernels[0]._npu_linearize:
4974 self._apply_linearize(kernels[0] if len(kernels) == 1 else None, node_schedule)5248 self._apply_linearize(kernels[0] if len(kernels) == 1 else None, node_schedule)
4975 5249 
4976 # r-axis cross-core split for OUTER reductions: when the x-axis core5250 # r-axis cross-core split for OUTER reductions: when the x-axis core
@@ -5125,8 +5399,16 @@ class NPUTritonScheduling(TritonScheduling):
5125 **partial.inductor_meta_common(),5399 **partial.inductor_meta_common(),
5126 }5400 }
5127 5401 
5402+ # Oversized block counts: a literal >= 2^31 types as uint32 in [2^31, 2^32) and poisons
5403+ # the div/mod dispatch chain with signedness errors; alias the i64
5404+ # runtime arg instead (x_total_hint is the sole x axis here).
5405+ x0numel_def = (
5406+ "x0numel = xnumel"
5407+ if int(x_total_hint) >= 2**31
5408+ else f"x0numel = {int(x_total_hint)}"
5409+ )
5128 pre_loop_lines = [5410 pre_loop_lines = [
5129- f"x0numel = {int(x_total_hint)}",5411+ x0numel_def,
5130 "real_block_x0 = x0numel if x0numel <= XBLOCK else XBLOCK",5412 "real_block_x0 = x0numel if x0numel <= XBLOCK else XBLOCK",
5131 "x0_blocks = (x0numel + real_block_x0 - 1) // real_block_x0",5413 "x0_blocks = (x0numel + real_block_x0 - 1) // real_block_x0",
5132 ]5414 ]
@@ -6989,7 +7271,7 @@ def {combine_name}(in_ptr0, out_ptr0, xnumel, r0_numel, XBLOCK : tl.constexpr, R
6989 """7271 """
6990 Override to track index_vars and var_ranges per node (needed for linearize mode).7272 Override to track index_vars and var_ranges per node (needed for linearize mode).
6991 """7273 """
6992- if triton_codegen_linearize:7274+ if kernel._npu_linearize:
6993 kernel.var_ranges_per_node = []7275 kernel.var_ranges_per_node = []
6994 kernel.index_vars_per_node = []7276 kernel.index_vars_per_node = []
6995 7277 
@@ -7025,7 +7307,7 @@ def {combine_name}(in_ptr0, out_ptr0, xnumel, r0_numel, XBLOCK : tl.constexpr, R
7025 indexing_dtype_strength_reduction(node._body)7307 indexing_dtype_strength_reduction(node._body)
7026 index_vars = kernel.split_and_set_ranges(node.get_ranges())7308 index_vars = kernel.split_and_set_ranges(node.get_ranges())
7027 7309 
7028- if triton_codegen_linearize:7310+ if kernel._npu_linearize:
7029 kernel.var_ranges_per_node.append(node.get_ranges())7311 kernel.var_ranges_per_node.append(node.get_ranges())
7030 kernel.index_vars_per_node.append(index_vars)7312 kernel.index_vars_per_node.append(index_vars)
7031 7313 
@@ -7119,6 +7401,14 @@ def apply_npu_codegen_patches():
7119 _patch_zero_dim_cpu_tensor_for_npu()7401 _patch_zero_dim_cpu_tensor_for_npu()
7120 7402 
7121 # NPU AI Vector Core does not natively support int64 arithmetic — demote to int32.7403 # NPU AI Vector Core does not natively support int64 arithmetic — demote to int32.
7404+ # Mapping ownership (R7 audit): this entry is process-global and shared
7405+ # with the default backend in mixed processes. Within triton_experimental
7406+ # it is now VESTIGIAL for int64 — dtype_to_str is overridden, and the
7407+ # store/value routes are patched (see npu_triton_store_type /
7408+ # _npu_value_expr); it survives only so the default backend's own
7409+ # consumers keep their behaviour. New direct triton_type(torch.int64)
7410+ # call sites surface through the callsite pin in
7411+ # test_triton_experimental_int32_overflow.
7122 import torch._inductor.utils as _inductor_utils7412 import torch._inductor.utils as _inductor_utils
7123 _inductor_utils._triton_type_mapping["tl.int64"] = "tl.int32"7413 _inductor_utils._triton_type_mapping["tl.int64"] = "tl.int32"
7124 # Also patch triton_compute_type which torch_npu overrides with its own7414 # Also patch triton_compute_type which torch_npu overrides with its own
@@ -197,6 +197,17 @@ def disable_pointwise_autotuning(inductor_meta):
197 return not inductor_meta.get("autotune_pointwise", True)197 return not inductor_meta.get("autotune_pointwise", True)
198 198 
199 199 
200+def _is_constexpr_signature_error(e: BaseException) -> bool:
201+ """The ONE ValueError that is a skippable autotune candidate: triton's
202+ ASTSource validates every Config constexpr against the kernel signature
203+ and raises "'<name>' is not in list" for fold candidates whose kernel
204+ body folds inline (no declared constexpr). A blanket ValueError catch
205+ would swallow real codegen bugs, so only this exact shape is
206+ candidate-level; both the serial and the parallel precompile paths must
207+ discriminate identically."""
208+ return isinstance(e, ValueError) and "is not in list" in str(e)
209+ 
210+ 
200def _fmt_config(cfg):211def _fmt_config(cfg):
201 try:212 try:
202 return (f"kwargs={dict(cfg.kwargs)} warps={getattr(cfg, 'num_warps', None)} "213 return (f"kwargs={dict(cfg.kwargs)} warps={getattr(cfg, 'num_warps', None)} "
@@ -435,7 +446,12 @@ class NPUTritonCompileResult(TritonCompileResult):
435 self.inductor_meta, def_args, fn.arg_names446 self.inductor_meta, def_args, fn.arg_names
436 )447 )
437 is_unsplit_scalar_reduction = (448 is_unsplit_scalar_reduction = (
438- npu_num_x_nodes == 0449+ # Non-linearize bodies index xoffset = pid*XBLOCK directly (no
450+ # group-dispatch odometer folding), so a grid of 1 would only run
451+ # the first tile — they must fall through to the exact-grid branch
452+ # below regardless of the x-node count.
453+ self.inductor_meta.get("npu_linearize", True)
454+ and npu_num_x_nodes == 0
439 and grid_type == "Grid1D"455 and grid_type == "Grid1D"
440 and "R0_BLOCK" in set(fn.arg_names)456 and "R0_BLOCK" in set(fn.arg_names)
441 and not npu_rsplit_partial457 and not npu_rsplit_partial
@@ -484,6 +500,20 @@ class NPUTritonCompileResult(TritonCompileResult):
484 # max/min run only when xnumel changes, still correct for any value. The cache500 # max/min run only when xnumel changes, still correct for any value. The cache
485 # cell is bound as a hidden default param (below) so the lookup is LOAD_FAST.501 # cell is bound as a hidden default param (below) so the lookup is LOAD_FAST.
486 grid_0_is_memoized = True502 grid_0_is_memoized = True
503+ elif not self.inductor_meta.get("npu_linearize", True) and grid_type == "Grid1D" and "xnumel" in def_args:
504+ # Non-linearize structure: xoffset = pid*XBLOCK with an always-true
505+ # xmask (xnumel % XBLOCK == 0) and no group-dispatch folding, so
506+ # the grid must be EXACTLY ceil(xnumel/XBLOCK) — one tile per pid.
507+ # Larger grids read past the input (MTE fault 507035, vector core
508+ # exception — codegen/triton.py sets inductor_meta["npu_linearize"]
509+ # =False for these kernels); smaller ones silently drop tiles
510+ # (uninitialized output rows, no fault — a min(ceil, NPU_CU_COUNT)
511+ # clamp regressed sum(64,128,256) at XBLOCK=128 where ceil=64 > 48).
512+ # Unlike the simple-1D pointwise branch there is no num_x_nodes
513+ # guarantee that ceil <= NPU_CU_COUNT, so no upper clamp: reductions
514+ # time-slice tiles over the cores instead.
515+ grid_0_expr = f"max(1, (xnumel + {xblock_val} - 1) // {xblock_val})"
516+ grid_0_is_memoized = True
487 else:517 else:
488 grid_0_expr = str(NPU_CU_COUNT)518 grid_0_expr = str(NPU_CU_COUNT)
489 grid_0_is_memoized = False519 grid_0_is_memoized = False
@@ -743,7 +773,11 @@ class NPUCachingAutotuner(CachingAutotuner):
743 compile_failed.append((c, e))773 compile_failed.append((c, e))
744 last_exc = e774 last_exc = e
745 except Exception as e:775 except Exception as e:
746- if isinstance(e, OutOfResources):776+ # ValueError with the constexpr-signature shape ("'X' is
777+ # not in list"): fold candidates whose kernel body folds
778+ # inline must be skipped, not fail the whole compile. Any
779+ # OTHER ValueError is a real codegen bug and must raise.
780+ if isinstance(e, OutOfResources) or _is_constexpr_signature_error(e):
747 log.debug(" [COMPILE FAIL] %s -> %s: %s", _fmt_config(c), type(e).__name__, e) # noqa: G200781 log.debug(" [COMPILE FAIL] %s -> %s: %s", _fmt_config(c), type(e).__name__, e) # noqa: G200
748 compile_failed.append((c, e))782 compile_failed.append((c, e))
749 last_exc = e783 last_exc = e
@@ -756,6 +790,13 @@ class NPUCachingAutotuner(CachingAutotuner):
756 return self._precompile_config(cfg), None790 return self._precompile_config(cfg), None
757 except (MLIRCompilationError, CompilationError, OutOfResources) as e:791 except (MLIRCompilationError, CompilationError, OutOfResources) as e:
758 return None, e792 return None, e
793+ except ValueError as e:
794+ # Same discrimination as the serial loop: only the
795+ # constexpr-signature shape is candidate-level, every other
796+ # ValueError is a real codegen bug and propagates.
797+ if _is_constexpr_signature_error(e):
798+ return None, e
799+ raise
759 800 
760 max_workers = min(compile_threads, len(configs))801 max_workers = min(compile_threads, len(configs))
761 with ThreadPoolExecutor(max_workers=max_workers) as executor:802 with ThreadPoolExecutor(max_workers=max_workers) as executor:
@@ -918,7 +959,16 @@ class NPUCachingAutotuner(CachingAutotuner):
918 compile_meta = copy.deepcopy(self.triton_meta)959 compile_meta = copy.deepcopy(self.triton_meta)
919 960 
920 cfg_kwargs = cfg.kwargs961 cfg_kwargs = cfg.kwargs
921- compile_meta["constants"].update(cfg_kwargs)962+ # Only kernel-declared constexprs may enter ASTSource constants. A
963+ # cache-loaded Config (read_best -> _load_cached_autotuning) can carry
964+ # foreign bookkeeping keys inside kwargs; triton's ast_to_ttir validates
965+ # every constants key against the kernel signature and rejects the rest
966+ # ("'X' is not in list"), killing every config at once. Filter at the
967+ # boundary so a dirty Config degrades to its constexpr payload only.
968+ constexpr_names = {self.fn.arg_names[i] for i in self.fn.constexprs}
969+ compile_meta["constants"].update(
970+ {k: v for k, v in cfg_kwargs.items() if k in constexpr_names}
971+ )
922 for i in self.fn.constexprs:972 for i in self.fn.constexprs:
923 arg_name = self.fn.arg_names[i]973 arg_name = self.fn.arg_names[i]
924 if arg_name not in compile_meta["constants"] and arg_name in (974 if arg_name not in compile_meta["constants"] and arg_name in (