Skip to content

Add a Python and cuTile backend and lowering pipeline - #2708

Open
Spielopoly wants to merge 106 commits into
spcl:extendedfrom
Spielopoly:cutile-submission-minimal
Open

Spielopoly wants to merge 106 commits into
spcl:extendedfrom
Spielopoly:cutile-submission-minimal

Conversation

@Spielopoly

@Spielopoly Spielopoly commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds a Python code-generation backend and cuTile support to DaCe. Supported computations in DaCe programs can now be lowered into tile-based GPU kernels, with generated Python code handling host execution, data movement, and kernel launches.

The implementation introduces three main components:

  • Python backend: Generates executable Python for control flow, maps, tasklets, and nested SDFGs, with NumPy and CuPy array management. This backend is also usable independently of cuTile.
  • cuTile code generation: Adds cuTile implementations of the existing tile operations, including arithmetic, conditional selection, gathers and scatters, and reductions.
  • VectorizeCuTile: Provides a single entry point that combines canonicalization, multidimensional vectorization, GPU data management, and cuTile lowering. It supports symbolic problem sizes and masked handling of incomplete tiles.

Generated programs can combine cuTile kernels with CuPy implementations of BLAS, linear algebra, and reductions.

For example, the following program computes a scaled addition of two matrices using symbolic dimensions and two-dimensional tiles:

import dace
import numpy as np
from dace.transformation.passes.vectorization import VectorizeCuTile

M = dace.symbol("M")
N = dace.symbol("N")

@dace.program
def scaled_add(
    alpha: dace.float32,
    A: dace.float32[M, N],
    B: dace.float32[M, N],
    C: dace.float32[M, N],
):
    C[:] = alpha * A + B

sdfg = scaled_add.to_sdfg()
VectorizeCuTile(widths=(16, 32)).apply_pass(sdfg, {})
compiled = sdfg.compile()

rng = np.random.default_rng(42)
shape = (257, 513)
a = rng.random(shape, dtype=np.float32)
b = rng.random(shape, dtype=np.float32)
c = np.empty_like(a)

compiled(
    alpha=np.float32(2.5),
    A=a,
    B=b,
    C=c,
    M=shape[0],
    N=shape[1],
)

VectorizeCuTile lowers the computation into cuTile Python kernels operating on 16 × 32 tiles. The generated program handles transfers for NumPy inputs and masks incomplete tiles at the array boundaries. Setting use_gpu_storage=True allows callers to pass CuPy arrays directly and keep data on the GPU between calls. Tile widths must be powers of two; problem sizes need not be divisible by those widths.

The example produces a cuTile Python kernel like the following (simplified for clarity):

import cuda.tile as ct
import cupy as cp

@ct.kernel
def scaled_add_kernel(A, B, alpha, C, M, N):
    M = ct.load(M, (0,), shape=()).item()
    N = ct.load(N, (0,), shape=()).item()
    alpha = ct.load(alpha, (0,), shape=()).item()

    tile_m = ct.bid(0)
    tile_n = ct.bid(1)

    rows = ct.arange(16, dtype=ct.int32) + tile_m * 16
    cols = ct.arange(32, dtype=ct.int32) + tile_n * 32
    rows = ct.broadcast_to(rows[:, None], (16, 32))
    cols = ct.broadcast_to(cols[None, :], (16, 32))
    mask = (rows < M) & (cols < N)

    a = ct.load(
        A, index=(tile_m, tile_n), shape=(16, 32),
        padding_mode=ct.PaddingMode.ZERO,
    )
    b = ct.load(
        B, index=(tile_m, tile_n), shape=(16, 32),
        padding_mode=ct.PaddingMode.ZERO,
    )
    a = ct.where(mask, a, 0)
    b = ct.where(mask, b, 0)

    scaled = ct.where(mask, alpha * a, False)
    result = ct.where(mask, scaled + b, False)
    ct.scatter(C, (rows, cols), result, mask=mask)


def scaled_add(A, B, C, M, N, alpha):
    A_gpu = cp.asarray(A)
    B_gpu = cp.asarray(B)
    C_gpu = cp.empty((M, N), dtype=cp.float32)
    grid = ((M + 15) // 16, (N + 31) // 32, 1)

    if min(grid) > 0:
        ct.launch(
            cp.cuda.get_current_stream(),
            grid,
            scaled_add_kernel,
            (
                A_gpu,
                B_gpu,
                cp.asarray(alpha, dtype=cp.float32).reshape(1),
                C_gpu,
                cp.asarray(M, dtype=cp.int32).reshape(1),
                cp.asarray(N, dtype=cp.int32).reshape(1),
            ),
        )

    cp.cuda.get_current_stream().synchronize()
    C_gpu.get(out=C)

Spielopoly added 30 commits June 2, 2026 05:34
Port the Python code generation backend and cuTile Python target from
the tile-ir branch onto the multi-dim-tileops base.

New files:
- dace/codegen/py/: Complete Python backend (target, framecode,
  cutile_target, control_flow, compiled_sdfg, prettycode, utils)
- tests/codegen/python_backend/: 16 test files (426 tests)

Modified files:
- dace/dtypes.py: BackendLanguage enum, CuTile_Tile/CuTile/GPU_Warp
  types, PYTHON_TYPES/NUMPY_TYPES dicts, gpuStream_t, can_access/
  can_allocate updates, struct.emit_python_definition()
- dace/sdfg/sdfg.py: backend property, _update_code() helper
  (backwards-compatible, defaults to C++), compile() Python path
- dace/codegen/codegen.py: Python backend dispatch via match statement
- dace/codegen/dispatcher.py: PythonCodeIOStream support,
  TargetCodeGeneratorBase type hints, GPUStream enum
- dace/codegen/target.py: Split TargetCodeGenerator into
  TargetCodeGeneratorBase (no registry) + TargetCodeGenerator (C++
  with registry and CMake support)
Integrate cuTile library node expansions with the VectorizeCPUMultiDim
pipeline and Python backend codegen. This enables end-to-end GPU
execution via cuda.tile.

Bug fixes:
- Fix connector name mismatches in all 9 cutile expansion classes
  (tile_load, tile_store, tile_binop, tile_unop, tile_mask_gen,
  tile_merge, tile_gather, tile_scatter, tile_reduce) to match
  library node connectors (e.g., _src/_dst instead of __src/__output)
- Add CUTILE to _VALID_ISAS in VectorizeCPUMultiDim
- Add Python target extension loop in codegen.py to instantiate
  CuTilePythonCodeGen (was never instantiated, dispatcher registrations
  never happened)
- Fix PythonCodeGen double-instantiation causing 'Schedule already
  mapped' errors
- Fix scope connector resolution in cutile_target.py _generate_Tasklet
  (MapEntry OUT_A -> actual array name A, MapExit IN_C -> array C)
- Add output pre-binding for store tasklets (ct.scatter needs _dst
  bound to global array before tasklet body executes)

Tests:
- 60 new integration tests (test_cutile_integration.py): full pipeline
  from SDFG -> vectorize -> cutile expand -> Python backend -> GPU run
- 30 unit tests (test_cutile_tasklet_scope_connectors.py): scope
  connector resolution
- Updated 28 existing structural tests for new connector names
  Introduce PythonTimerProvider, a timing instrumentation provider for
  the Python codegen backend. It uses a compiled C++ shared library
  (_dace_timer.so) loaded via ctypes for high-resolution timestamps,
  falling back to time.perf_counter_ns when compilation is unavailable.

  Supported instrumentation levels:
  - SDFG-level (whole program)
  - State-level (individual states)
  - Scope-level (maps / nested SDFGs)

  Reports are written in Chrome Tracing JSON format, compatible with
  sdfg.get_latest_report() and the existing C++ backend report API.

  PythonCompiledSDFG gains _cfunc, _libhandle, and do_not_execute
  attributes for compatibility with CompiledSDFGProfiler (npbench).

  The provider is swapped in automatically by codegen.py when
  sdfg.backend == BackendLanguage.Python, replacing TimerProvider.

  New files:
  - dace/codegen/instrumentation/_dace_timer.cpp
  - dace/codegen/instrumentation/py_timer.py
  - tests/codegen/python_backend/test_py_instrumentation.py

  Modified:
  - dace/codegen/codegen.py (provider swap logic)
  - dace/codegen/py/framecode.py (SDFG/state hooks)
  - dace/codegen/py/python_target.py (scope entry/exit hooks)
  - dace/codegen/py/compiled_sdfg.py (profiler compatibility)
Rework PythonTimerProvider to reach parity with the C++ TimerProvider while
removing accidental complexity, and register it cleanly instead of overriding.

Provider selection
- Add InstrumentationType.PythonTimer and register PythonTimerProvider against
  it via @registry.autoregister_params (no collision with the C++ Timer type).
- Drop the backend-aware get_provider_mapping(backend=...) override and the
  post-hoc isinstance swap in codegen.py. Select PythonTimer explicitly on the
  element to time (e.g. sdfg.instrument = InstrumentationType.PythonTimer).

Timer source
- Delete the compiled C++ shared library (_dace_timer.cpp) and its ctypes/
  subprocess machinery; use time.perf_counter_ns()//1000 unconditionally
  (identical microsecond resolution).

Emission & per-invocation semantics
- Emit timer/event-list/import setup once at module scope for the top-level
  SDFG only (no redundant per-SDFG emission).
- Honor instrumentation.report_each_invocation:
  * true (default): clear __dace_perf_events per call + save a fresh report
    each call (parity with C++ report.reset() + per-call save). Fixes a bug
    where events accumulated across invocations.
  * false: accumulate across calls and write one report from the generated
    __dace_exit_<name>() function (run by PythonCompiledSDFG.finalize()/__del__),
    the analog of C++ __dace_exit. os/json/time are imported once at module
    scope so the exit-function body does no imports � avoids an __import__-hook
    failure at interpreter shutdown (sys.meta_path is None).

Granularity
- Implement node-level (tasklet) timing for the plain Python target.
- Reject node-level instrumentation inside cuTile kernels (RuntimeError).
- Make cuTile kernels timeable: always emit a stream synchronize() after
  ct.launch, and wrap launch+sync with scope timing (pass the exit node to
  on_scope_exit so tbegin/tend ids match).

Misc
- Add TODOs for the still-missing on_sdfg_init_*/on_sdfg_exit_* hook dispatch
  in the Python frame code (and where false-mode should move once they exist).
- Tests: end-to-end runtime coverage (SDFG/state/map/node, per-invocation
  reset, symbolic + non-divisible sizes, report schema, frozen sdfgHash),
  real-GPU cuTile runtime tests, cuTile structural checks, report_each_invocation
  (true/false), and provider registration.

2. Added: TODOs for init/exit hook dispatch

Three TODOs now mark the missing on_sdfg_init_*/on_sdfg_exit_* dispatch and tie it to the false-mode wiring:
- framecode.py generate_code (next to the on_sdfg_begin dispatch).
- framecode.py _build_lifecycle_functions (where __dace_init/__dace_exit are built � the natural dispatch point).
- py_timer.py on_sdfg_end (noting the false-mode save should move from reaching into codegen._exitcode int hooks exist).

Tests & docs

- Updated the two false-mode tests in test_py_instrumentation_2.py: E2 now asserts the writer lives in __dace_exit_<name>() (no atexit); E4 calls csdfg.finalize() and checks the accumulated report.
- Both instrumentation test files: 80 passed; full python_backend suite: 547 passed, 1 skipped, 9 xfailed  noise.
- Updated the design doc (TileIR/design/PYTHON_INSTRUMENTATION_DESIGN.md §4, §8, §9), CLAUDE.md (and corrected its stale doc path), and the session report.

One note carried into the docs as a known limitation: at interpreter shutdown specifically, the flush ridert (true of any finalizer doing I/O); finalize() and mid-program GC are deterministic.

� Sautéed for 11m 23s

� recap: Goal: refactor the DaCe Python-backend timing instrumentation to match the C++ backend. The provider now uses a dedicated PythonTimer type, and false-mode reports save via the SDFG finalizer; all 547 backend tests pass. Next: confirm with you whether anything else is needed.

� Give me a good commit message for both TileIR and dace

Let me check what's actually changed in each repo so the messages are accurate.

  Ran 2 shell commands

t changed in each.

dace repo

refactor(codegen): overhaul Python-backend timing instrumentation

Rework PythonTimerProvider to reach parity with the C++ TimerProvider while
removing accidental complexity, and register it cleanly instead of overriding.

Provider selection
- Add InstrumentationType.PythonTimer and register PythonTimerProvider against
  it via @registry.autoregister_params (no collision with the C++ Timer type).
- Drop the backend-aware get_provider_mapping(backend=...) override and the
  post-hoc isinstance swap in codegen.py. Select PythonTimer explicitly on the
  element to time (e.g. sdfg.instrument = InstrumentationType.PythonTimer).

Timer source
- Delete the compiled C++ shared library (_dace_timer.cpp) and its ctypes/
  subprocess machinery; use time.perf_counter_ns()//1000 unconditionally
  (identical microsecond resolution).

Emission & per-invocation semantics
resh report
    each call (parity with C++ report.reset() + per-call save). Fixes a bug
    where events accumulated across invocations.
  * false: accumulate across calls and write one report from the generated
    __dace_exit_<name>() function (run by PythonCompiledSDFG.finalize()/__del__),
    the analog of C++ __dace_exit. os/json/time are imported once at module
    scope so the exit-function body does no imports � avoids an __import__-hook
    failure at interpreter shutdown (sys.meta_path is None).

Granularity
- Implement node-level (tasklet) timing for the plain Python target.
- Reject node-level instrumentation inside cuTile kernels (RuntimeError).
- Make cuTile kernels timeable: always emit a stream synchronize() after
  ct.launch, and wrap launch+sync with scope timing (pass the exit node to
  on_scope_exit so tbegin/tend ids match).

Misc
- Add TODOs for the still-missing on_sdfg_init_*/on_sdfg_exit_* hook dispatch
  in the Python frame code (and where false-mode should move once they exist).
- Tests: end-to-end runtime coverage (SDFG/state/map/node, per-invocation
  reset, symbolic + non-divisible sizes, report schema, frozen sdfgHash),
  real-GPU cuTile runtime tests, cuTile structural checks, report_each_invocation
  (true/false), and provider registration.
…trided loads, permutations, scalars and symbols
…(True) == hash(1), which will cause the function to return whichever was passed first instead of the actual value
…ementations casted to ints, which is actually unnecessary, so it's removed now.
Spielopoly and others added 18 commits July 3, 2026 01:33
# Conflicts:
#	dace/codegen/py/cutile_target.py
#	dace/transformation/passes/vectorization/vectorize_cutile.py
Code-review fixes for the vectorization / validation passes:

- clamp_propagated_oob_memlets: gate the clamp so only tileops-adjacent
  or allow_oob-marked memlets are narrowed. Genuine unmasked OOB memlets
  are left untouched (with a diagnostic warning) so validation rejects
  them loudly instead of silently compiling a truncated footprint.
  Also recompute memlet.volume after narrowing, clamp other_subset under
  the same gating, and treat NestedSDFG boundary edges feeding tileops
  nodes (nest_map_bodies=True descent) as tileops-adjacent (mark
  allow_oob, never narrow below W).
- TileIota cutile expansion: derive the ct.arange lane-array dtype from
  the _dst descriptor (int64 -> ct.int64) so descriptor and runtime tile
  dtype agree.
- validation.py: InvalidSDFGNodeError/InvalidSDFGEdgeError __str__ stale-id
  branches now fall through to the common suffix logic so the "saved for
  inspection" path is not skipped; fix the locinfo_dst formatting branch in
  InvalidSDFGInterstateEdgeError.__str__.
- Document the 2b/3b steps in the cutile_lowering module docstring and the
  propagate_subset allow_oob-drop caveat in mark_tile_op_memlets_allow_oob.

Regression tests: genuine-OOB-still-rejected, volume recomputation,
other_subset clamping, NSDFG boundary marking (+ GPU e2e nest_map_bodies
tail), propagate_subset flag-drop caveat, TileIota int64 codegen (+ GPU
numeric), and validation error __str__ suffix rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Majors:
1. compiled_sdfg._marshal_arguments: raise TypeError on dtype-mismatched
   ndarray Scalar args (silent copy would drop writeback) and on lossy
   scalar-value coercion (3.9 -> int no longer silently truncates).
2. cutile_target._collect_free_symbols: subtract scope-bound map params
   (including nested-map params) before the runtime_defined filter, so a
   map param colliding with a host loop var / interstate key is not emitted
   as an undefined launch arg.
3. cutile_target._cast_literal_arm: don't cast a fractional float literal to
   an integer tile dtype (silent truncation); handle signed literals
   (UnaryOp USub/UAdd) like Constants.
4. cutile_target: in-kernel float-scalar tile binding now mirrors the launch
   site (float-scalar 0-d tile load only when the scalar is not a kernel
   output); float Scalar in input∩output raises NotImplementedError.
5. cutile_target.generate_scope: float64 symbols staged through device memory
   (1-elem array + 0-d tile load) like float Scalars, avoiding f32 precision
   loss on by-value floats.

Minors:
6. Deleted unreachable cpu_to_gpu Scalar branch in _emit_cross_storage_copy.
7. Deleted dead _scalar_bridge_load_expr; clarified the 0-d comment (concerns
   0-d ARRAYS, not shape=() loads of 1-element arrays).
8. compiled_sdfg.__call__ fast path: only swallow argname-resolution failure;
   binding errors propagate; positional Scalars still marshalled.
9. cholesky ExpandCholeskyCuPy: filter strides by kept (non-squeezed) dims,
   fixing add_array on batched/leading-singleton inputs.
10. _CuTileTaskletRewriter: taint pass so ternaries conditioned on a
    tasklet-local tile value are rewritten to ct.where.
11. visit_Call: probed on GPU that ct.* math accepts plain scalars; kept the
    unconditional rewrite, documented the probe.

Suggestions:
12. ITE: coerce a host condition into device arms' namespace before np.where.
13. Launch guard: min((...)) > 0 skips negative-trip grids, not just zeros.
14. Documented the scalar branches in _emit_cross_storage_copy.
15. _bind_positional raises TypeError on excess positional args.
16. Strengthened the near-vacuous broadcast-assignment marshalling assert.

Regression tests added/extended: argument marshalling (dtype mismatch, lossy
coerce, view writeback, excess/duplicate positional), tasklet rewrite
(signed/fractional literals, tainted-local ternaries), scope codegen
(colliding map params, float in/out guard, float64 symbol staging, launch
guard), ITE host/device coercion, cholesky batched-stride expansion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ution)

Resolved every conflict — git-flagged and silent — in favour of
vectorized-tile-ir/extended, discarding the npbench-cutile side:

- 6 content conflicts (scalar.h, expand_nested_sdfg_inputs.py + test,
  split_tasklets.py + test, convert_tasklets_to_tile_ops.py,
  insert_tile_load_store.py) taken wholesale from VTI via `-X ours`;
  NBC's conflicting hunks (ranged other_subset guard, >1 masked-assign
  guard, sanitize_transient_name_hint import, TileIota lane-id block)
  dropped.
- Modify/delete tile_carried_reduction.py: kept VTI's deletion.
- NBC's non-conflicting additions (Python cuTile backend, cutile
  lowering, device-resident CuPy BLAS, offset recovery) merged in.

Known consequences of the VTI-wins policy (see the 2026-07-06 overlap
report §3.3): SC-A (VTI dropped "CUTILE" from _VALID_ISAS -> NBC's
cuTile front door raises NotImplementedError at construction) and the
SC-1/2/3/5 silent semantic gaps remain unaddressed by design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Cg7RaMAnJpH9EC8yAdNk5
Post-merge fixes on top of the -X ours merge (0525049):

1. NameError fallout of `-X ours` (it wins conflicting hunks but keeps
   NBC's disjoint hunks, leaving dangling refs). Restored both files to
   pure vectorized-tile-ir (1e20822):
   - insert_tile_load_store.py: NBC's 5 sanitize_transient_name_hint()
     callsites had merged in while the import was dropped -> NameError on
     every tile-read staging. Confirmed via test_return_array_naming.
   - convert_tasklets_to_tile_ops.py: NBC's `add_edge(iota, ...)` line
     merged onto VTI's `tasklet` builder -> undefined `iota`.
   Verified: test_k0_remainder (16) + lower_reduction_wcr (5) pass.

2. SC-A: re-added "CUTILE" to _VALID_ISAS + the constructor Literal in
   vectorize_multi_dim.py (VTI had dropped it, renaming the slot to the
   C++ half2 "CUDA" backend). CUTILE stays a valid ISA alongside CUDA but
   is exempt from the half2 even-width constraint and does not force
   device=GPU at stage 1 (VectorizeCuTile's apply_gpu_transformations
   handles GPU-ification separately, matching base behaviour).
   Verified: NPBench dace_cutile atax validates SUCCESS on all 8 widths.

Known remaining (not in scope of these two fixes):
- Return-array kernels: pure VTI lacks NBC's sanitize, so a `__return`
  output collides ('__return_tile_out' reserved-name AssertionError).
  NBC's dropped fix was actually needed here.
- Pre-existing VTI circular import (vectorize_multi_dim.py:83 -> interstate),
  order-dependent; breaks standalone collection of some test modules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Cg7RaMAnJpH9EC8yAdNk5
Both follow-ups from the merge validation:

1. Return-array naming (insert_tile_load_store.py): ported NBC's
   sanitize_transient_name_hint — restored the module import (dropped by
   the -X ours conflict resolution) and re-wrapped the 5 name_hint sites.
   A `__return`-output kernel previously hit
   `AssertionError: '__return_tile_out' collides with the reserved __...`
   because pure VTI minted `f"{an.data}_tile_out"` verbatim. The sanitizer
   rewrites a leading `__return` to `tile_return`; behaviour-preserving for
   all other names. Verified: test_return_array_naming 2/2 pass.

2. Circular import (vectorize_multi_dim.py): moved the three module-level
   `dace.transformation.interstate` imports (InlineSDFG, InlineMultistateSDFG,
   LoopToMap, RefineNestedAccess, ExpandNestedSDFGInputs) to their function
   use-sites. A module-level import re-entered the partially-initialized
   interstate package on some import orders, breaking standalone collection
   of e.g. masked_write_it_lowering_test. (Pre-existing on VTI — the earlier
   "circular import resolved" note was wrong; VTI had moved it into this new
   split-out file.) Verified: that module now collects 10 tests.

Note: masked_write's AVX512-parametrized subtests SIGILL on this box —
the CPU has only avx2 (no AVX-512). Pre-existing test-vs-hardware mismatch,
unrelated to the merge; SCALAR variants pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Cg7RaMAnJpH9EC8yAdNk5
VTI keeps dtype casts rather than stripping them (it deleted NBC's
RemoveFPTypeCasts/RemoveIntTypeCasts), but the cuTile backend had no cast
lowering, so a surviving cast broke it two ways. Following VTI's keep-casts
design, teach the cuTile backend to lower casts:

- tile_unop.py ExpandTileUnopCutile: a TileUnop whose op is a dtype name now
  lowers to `ct.astype(x, ct.<dtype>)` (mirrors the pure path's dace::<dtype>(x)
  and the existing `not` -> ct.astype(_a, ct.bool_)). Fixes the tiled-cast
  `KeyError: 'float64'` (deriche, hdiff, mlp).
- cutile_target.py _CuTileTaskletRewriter: rewrite a module-qualified
  `dace.<dtype>(x)` / `np.<dtype>(x)` cast call in a kernel tasklet to
  `ct.astype(x, ct.<dtype>)`.
- python_target.py: rewrite `dace.<dtype>(x)` -> `numpy.<dtype>(x)` in host
  Python tasklet bodies (the generated module imports numpy, not dace). Fixes
  the `name 'dace' is not defined` scalar-cast failure (adi's np.float64(N)).

Verified: deriche/hdiff/mlp/adi validate under NPBench dace_cutile -p S,
matching the pre-merge npbench-cutile baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Cg7RaMAnJpH9EC8yAdNk5
…over cloudsc, icon)

The TileStore "full-tile write" validation rejected genuine full-tile stores
whose memlet extent was e.g. ``jl - jl + 512`` -- two same-name ``jl`` symbols
carrying different sympy assumptions (a nonnegative tile base vs. the loop's
plain ``jl``), which blocks ``jl - jl`` from cancelling, so ``symbolic.simplify``
left it non-zero and the check wrongly raised the design-6.7 NotImplementedError.

These are masked in-place 2-D elementwise updates (``ptend_q[jk, jl] = ...``)
tiled 1-D over the inner loop, so the store into the 2-D array is a legitimate
``(1, W)`` full-tile write. VTI's design (full-tile writes pass, partial writes
deferred) is unchanged; only the extent comparison is made assumption-robust by
re-parsing the difference from its string form (unifies symbols by name). Genuine
partial/single-element stores still fail (non-symbolic size mismatch).

Verified: cloudsc_tidy_branch and icon_zekinh_gather validate under NPBench
dace_cutile -p S, matching the pre-merge npbench-cutile baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Cg7RaMAnJpH9EC8yAdNk5
…ions

Re-introduce the canonicalize pipeline as the default-on step 0 of
VectorizeCuTile (run_canonicalize knob) with the cuTile knob row
(semantic_lifting/assumption_guard/reduction_to_wcr_map=False, peel_limit=0),
and fix the resulting NPBench cuTile regressions so canon-ON now passes
37/55 kernels vs the 36 of the canon-OFF baseline.

Fixes:
- tile_mask_gen: mask base uses the iteration variable (absolute), not
  __pid*W, so nonzero-begin ranges (1:N-1) mask correctly (adi tail write).
- tile_reduce: ct.astype identity literals use positional dtype to match
  the cuda.tile runtime signature (astype(x, dtype, /)).
- symbol-leak lowering (tile_binop/tile_unop free_symbols, tile_reduce
  dtype string), softmax non-packed stride (_pure_codegen ipow normalize),
  mvt connector force-add (normalize_map_body), lu None<str (fuse_consecutive
  _loops), crc16 condition snapshot (branch_normalization/flatten_branches),
  covariance RemoveViews other_subset rewrite, canonicalize timeouts
  (peel_limit=0), mixed-rank tile binding (cutile_target).

gramschmidt (sequential reduction nested in sequential loops, no parallel
grid map) is left as a documented cuTile-unfit holdout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017a2V8Hi7woWpHdGvHzNXpw
Pass all scalars as arrays with one lement in them to preserve their dtype
Copilot AI balanced review requested due to automatic review settings October 16, 2026 12:23
@Spielopoly Spielopoly closed this Oct 16, 2026
@Spielopoly Spielopoly reopened this Oct 16, 2026
@Spielopoly
Spielopoly marked this pull request as draft October 16, 2026 12:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Checkpoint the symbol migration before integrating extended. This preparation depends on extended's symbol repository and is not runnable against the old core alone. Full-suite baseline and dependency failures are recorded in the adjacent test-results directory.
Adapt Python and cuTile code generation to scoped symbol repositories and
upstream tile-operation and vectorization interfaces. Preserve upstream
core transformations and the cuTile and CuPy implementations.

Keep runtime symbols in inline tile expressions, handle scalar reduction
stores and host-driver temporaries, and update test fixtures to the new
state and nested-descriptor interfaces.

Focused validation: Python backend 914 passed, 1 skipped, 9 xfailed;
cuTile selection 438 passed, 3 pre-existing failures, 7 skipped.
@Spielopoly Spielopoly changed the title Add the Python/cuTile backend and lowering pipeline Add a Python and cuTile backend and lowering pipeline Oct 18, 2026
@Spielopoly
Spielopoly marked this pull request as ready for review October 18, 2026 01:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants