Skip to content

[Perf] Tiles 1: _load, _store, _eye_#466

Merged
hughperkins merged 23 commits into
mainfrom
hp/tiles-1
Apr 12, 2026
Merged

[Perf] Tiles 1: _load, _store, _eye_#466
hughperkins merged 23 commits into
mainfrom
hp/tiles-1

Conversation

@hughperkins

@hughperkins hughperkins commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

Add the foundation for register-resident 16x16 tile operations:

  • _make_tile16x16(dtype) factory creating @qd.dataclass with r0–r15
  • _load/_store/_load3d/_store3d with row and column bounds checking
  • eye_() for in-place identity initialization
  • zeros/eye factories

Issue: #

Brief Summary

Description from Opus:

for the purposes of pr description, describe what is in pr 1, and the good and bad points about pr 1

What's in PR 1

Introduces Tile16x16, a register-resident 16x16 matrix tile distributed across 16 subgroup threads. Each thread owns one row stored in 16 scalar registers (r0-r15). This is the
foundation for blocked linear algebra kernels (Cholesky, triangular solve) that follow in later PRs.
Files changed (614 lines, 4 files):
_tile16.py (324 lines) -- The tile dataclass with _load/_store (2D), _load3d/_store3d (3D), and eye methods. A _make_tile16x16(dtype) factory creates dtype-specialized
tile types, cached to avoid re-creation. Both f32 and f64 are supported.
type_check.cpp (4 lines) -- Fixes InternalFuncStmt type checking to preserve polymorphic return types (e.g. subgroupShuffle returning f32 instead of being hardcoded to
i32). Required for tile shuffles to work correctly.
test_tile16.py (276 lines) -- Tests for zeros, eye, eye-inplace, parametrized load/store (various offsets, window sizes, partial rows/cols), 3D load/store, and array-shape
clamping (both row and column).
test_utils.py (12 lines) -- Shared skip_if_f64_unsupported helper.

────────────────────────────────────────

Good points

• Correct bounds clamping in both dimensions -- _load/_store clamp both row_end and col_end to the actual array shape, preventing out-of-bounds GPU memory access when the tile
window extends past the array.
• Thoroughly parametrized tests -- Load/store tests cover 8 combinations of offsets, partial rows, partial columns on a 92x92 grid, all crossed with f32/f64. Edge cases (array
narrower/shorter than 16) are explicitly tested.
• Clean internal API -- Everything is private (_tile16.py, _load, _store, eye). The public API (Tile16x16 proxy with slice syntax) is deferred to later PRs, keeping this PR
focused.
• No shared memory needed -- The tile design uses only scalar registers and subgroup shuffles, making it portable across CUDA, Vulkan, and Metal.
• The type_check.cpp fix is minimal and correct -- It removes a hardcoded i32 return type that would silently corrupt f32/f64 shuffle results. The fix relies on ret_type
already being set correctly during IR flatten, which is guaranteed by InternalCallOperation::flatten.

Bad points

• Extremely repetitive -- _load, _store, _load3d, _store3d, and eye each repeat the same pattern 16 times for r0 through r15. The four load/store methods alone are ~220
lines of near-identical code. This is an inherent consequence of the DSL not supporting computed field access, but it makes the file hard to review and maintain.
_load3d/_store3d are near-duplicates of _load/_store -- The only difference is an extra batch index and arr.shape[2] vs arr.shape[1]. If the DSL supported 2D views
from 3D arrays, these could be eliminated entirely.
• Parameter order is non-standard -- _load(arr, row_start, col_start, col_end, row_end) interleaves row and column parameters instead of grouping them (row_start, row_end,
col_start, col_end). Easy to accidentally swap col_end and row_end when calling the internal API directly.
_tile16_cache is never invalidated -- The module-level cache persists across qd.reset() / qd.init() cycles. This appears to work in practice because the cached StructType
is just a type descriptor, but it's fragile and undocumented.
• No assertion if row_end - row_start > 16 -- Passing a range larger than 16 rows silently ignores rows beyond row_start + 15. This is correct behavior for a 16-row tile,
but a caller mistake here would be hard to debug.

Fix the parameter order

All call sites now use the new order (arr, row_start, row_end, col_start, col_end).

what are your thoughts on 'No assertion if row_end - row_start > 16 -- Passing a range larger than 16 rows silently ignores rows beyond row_start + 15. This is correct behavior for a 16-row tile, but a caller mistake here would be hard to debug.'?

Thinking about it more, I don't think it's worth adding. A few reasons:

  1. It's symmetric with columns. If you pass col_end - col_start > 16, only 16 columns are loaded too (there are only 16 registers). No assertion there either. Adding one for
    rows but not columns would be inconsistent, and adding both would be annoying.
  2. The silent clamping is actually convenient. In a tiled loop over a matrix, the last tile often has row_end = total_rows without bothering to compute min(row_start + 16,
    total_rows). The tile naturally handles this by only loading the 16 rows it can hold. Asserting would force every caller to do that clamping manually.
  3. The slice syntax in PR 3+ makes it moot. Users write t[:] = arr[r:r+16, c:c+16] and the proxy constructs correct bounds. Nobody calls _load directly except internal code.

I'd remove it from the "bad points" list. It's working as designed.

To Opus: "tell the reviewer a story about PR 1. Assume they know nothing about this code, but they do know high level quadrants concepts, as a user. Walk through: │

  • what challenge the PR is aiming to solve
  • how it solves it
  • how each bit of change code implements this, presneting in a logical order, so that all facts known so far are sufficient to underestand │
    The whole story should be readable in order, without needing to skip ahead to find information that hasn't been presented yet.”
 python/quadrants/lang/simt/_tile16.py lines 44-48

          r0: dtype
          r1: dtype
          // ... r2 through r13 ...
          r14: dtype
          r15: dtype

  The dtype parameter (e.g. qd.f32, qd.f64) comes from a factory function _make_tile16x16(dtype) that creates a specialized tile type for each scalar type. Results are cached to
  avoid recreating the same type:

   python/quadrants/lang/simt/_tile16.py lines 24-29

  def _make_tile16x16(dtype=qd.f32):
      if dtype in _tile16_cache:
          return _tile16_cache[dtype]
      cls = _make_tile16x16_class(dtype)
      _tile16_cache[dtype] = cls
      return cls

  Constructing a tile with Tile() gives you a zero tile (the dataclass defaults all fields to 0). An eye factory creates an identity tile via a module-level function attached to
  the type:

   python/quadrants/lang/simt/_tile16.py lines 317-324

      @qd.func
      def _eye():
          t = result()
          t._eye_()
          return t
      result.eye = _eye
      return result



  Loading and storing

  To be useful, tiles need to move data to/from arrays. Each of the 16 threads loads or stores its own row, so a full tile load/store is 16 parallel row accesses with no
  synchronization needed.
  _load(arr, row_start, row_end, col_start, col_end) reads a rectangular region from a 2D array into the tile's registers. Thread tid loads arr[row_start + tid,
  col_start:col_end]. Both dimensions are cl

              arr_row_end = arr.shape[0]
              if arr_row_end < row_end:
                  row_end = arr_row_end
              row = row_start + qd.simt.subgroup.invocation_id()
              if row < row_end:
                  arr_col_end = arr.shape[1]
                  if arr_col_end < col_end:
                      col_end = arr_col_end

  Each column is then loaded individually with a bounds check (if col_start + i < col_end), since the DSL doesn't support computed field access -- you can't write self.r{i} in a
  loop, so all 16 registers are written out explicitly. _store is the mirror image.
  The 3D variants (_load3d, _store3d) add a batch index for batched operations: arr[batch, row, col] instead of arr[row, col].
  The parameter order (row_start, row_end, col_start, col_end) groups by dimension, matching numpy's arr[row_start:row_end, col_start:col_end] convention.


  The type_check.cpp fix

  To make tiles work with f32 data, subgroup shuffles (used internally by later PRs) need to return the correct scalar type. Before this PR, InternalFuncStmt::visit in
  type_check.cpp hardcoded the return type to i32:

  -    stmt->ret_type = PrimitiveType::i32;
  +    // ret_type is already set correctly during flatten via
  +    // InternalCallOperation::flatten and the operation's type_check.

  The flatten pass already sets the return type correctly from the operation's polymorphic signature (e.g. subgroupShuffle returns ValueT, resolved from the argument type). The
  old code overwrote this with i32, silently corrupting f32/f64 shuffle results. The fix simply stops overwriting it.


  Privacy

  Everything in this PR is internal. The module is named _tile16.py (leading underscore), all methods are prefixed with _, and it's not exported from __init__.py. The user-facing
   API (slice syntax like t[:] = arr[r:r+16, c:c+16] and the Tile16x16 proxy class) comes in later PRs in this chain. This PR establishes the register layout and I/O primitives
  that those later PRs build on.


  Tests

  Tests use the internal _make_tile16x16 factory directly. All tests are parametrized over f32 and f64, with f64 skipped on Metal (unsupported) and Vulkan (unreliable) via a
  shared skip_if_f64_unsupported helper added to test_utils.py.
  The test suite covers:
  • Zero and identity tiles -- verify Tile() produces zeros, Tile.eye() produces identity, and _eye_() overwrites existing data.
  • Load/store round-trip -- parametrized across 8 combinations of source offsets, destination offsets, partial row counts, and partial column counts on a 92x92 grid. Verifies
    that exactly the requested rectangle is copied and nothing else is touched.
  • Array-shape clamping -- loads from / stores to arrays shorter than 16 rows, verifying that excess tile rows stay zero and no out-of-bounds writes occur. (Matching
    column-clamping tests are in the next PR that adds slice syntax.)
  • 3D batch dimension -- parametrized across batch indices, offsets, and partial windows on a (6, 92, 92) array.

copilot:summary

Walkthrough

copilot:walkthrough

Add the foundation for register-resident 16x16 tile operations:
- _make_tile16x16(dtype) factory creating @qd.dataclass with r0–r15
- _load/_store/_load3d/_store3d with row and column bounds checking
- eye_() for in-place identity initialization
- zeros/eye factories
- Fix InternalFuncStmt type_check to preserve polymorphic return types
Comment thread tests/python/test_tile16.py Outdated
Comment thread tests/python/test_tile16.py Outdated
Comment thread tests/python/test_tile16.py Outdated
Comment thread tests/python/test_tile16.py Outdated
Comment thread tests/python/test_tile16.py Outdated
- Import _TILE from tile16 instead of hardcoding
- Replace 3 separate load/store tests with parametrized test over 92x92 array
- Parametrize 3D load/store test over batches, offsets, and window sizes
Comment thread tests/python/test_tile16.py Outdated
np.testing.assert_allclose(dst.to_numpy(), np.eye(_TILE, dtype=np.float32))


_GRID = 92

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

move this inside each test metdho

Comment thread python/quadrants/lang/simt/tile16.py Outdated

from quadrants.lang.simt.tile16 import _make_tile16x16
Tile = _make_tile16x16(qd.f32)
TILE = Tile.SIZE

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

should this SIZE be private? Or is it the same one we use later? If not it should be priate.

Comment thread python/quadrants/lang/simt/tile16.py Outdated
The thread's lane index (tid) is obtained internally via subgroup.invocation_id(),
so callers never need to pass it. Load/store methods take a row offset (row0);
each thread accesses row = row0 + tid.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

STate that everythign is private, because hti is an internal API, wrapped by th elater Tile proxy classes.

@hughperkins hughperkins changed the title [Perf] Tiles 1: add Tile16x16 core — register-resident 16x16 tile dataclass [Perf] Tiles 1: _load, _store, _eye_ Apr 9, 2026
Comment thread python/quadrants/lang/simt/tile16.py Outdated
t._store(arr, row0, col0, col_stop, row_stop) # store to 2D array
t._load3d(arr, i0, row0, col0, col_stop, row_stop) # load from 3D array
t._store3d(arr, i0, row0, col0, col_stop, row_stop) # store to 3D array
t.eye_() # set to identity matrix (in-place)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

shouldnt eye_ be private? ie _eye_?

Comment thread quadrants/transforms/type_check.cpp Outdated
void visit(InternalFuncStmt *stmt) override {
// TODO: support return type specification
stmt->ret_type = PrimitiveType::i32;
// ret_type is already set correctly during flatten via

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why this change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

opus:

The old code hardcoded every InternalFuncStmt return type to i32, overriding whatever was set during IR flattening. This breaks subgroupShuffle — the shuffle operation returns
the same type as its input (e.g. f32 for float tiles), but the type checker was forcing it to i32.
Without this fix, every warp shuffle in the tile operations would be type-checked as returning an integer, causing miscompilation. The ret_type is already set correctly by
InternalCallOperation::flatten and the operation's own type_check, so the override just needs to be removed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

in this pr are we tesing both f32 and f64?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

opus:

No — PR 1 only tests f32. The f64 parametrization is in the full test suite introduced in PR 4 (which has @pytest.mark.parametrize("qd_dtype", [qd.f32, qd.f64])).
Do you want me to add f64 coverage to PR 1's tests?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

oes this pr support both f32 and f64?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opus:

Yes — the code supports both. _make_tile16x16(dtype) accepts any scalar dtype (qd.f32 or qd.f64), and all register fields are declared with the passed dtype. The tests just
only exercise f32. Want me to parametrize the PR 1 tests over both?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes

Comment thread python/quadrants/lang/simt/tile16.py Outdated
The old names (row0, col0, n_cols, row_stop) were misleading — n_cols
was actually an absolute column boundary, not a count. Fix the tests
to pass absolute boundaries (col_start + ncols) instead of counts.
Comment thread python/quadrants/lang/simt/__init__.py Outdated
from quadrants.lang.simt import block, grid, subgroup, warp

__all__ = ["warp", "subgroup", "block", "grid"]
__all__ = ["warp", "subgroup", "block", "grid", "tile16"]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

should this be private?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opus:

On PR 1, qd.simt.tile16 is exposed in init.py via lazy loading, but the only consumer is the test file — and the test imports directly from quadrants.lang.simt.tile16, not
through qd.simt.tile16.
So no, it doesn't need to be public in PR 1. We could remove it from all and the getattr lazy loader, deferring that to PR 4 when the public Tile16x16 proxy is
introduced. Want me to make that change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why does Tile16x16 proxy need tile16 to be public?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opus:

It doesn't. Nothing uses qd.simt.tile16 as a public module — every consumer imports directly from the full path quadrants.lang.simt.tile16. The getattr lazy loader for
"tile16" in init.py is dead code.
So we can remove tile16 from all and the getattr block entirely, on all branches. Want me to do that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes. so then it would not be publicly visible? or do we need to rename to _tile16 too?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opus:

Removing it from all and getattr means qd.simt.tile16 won't resolve — users can't discover or access it through the public API. That's the main thing.
However, from quadrants.lang.simt.tile16 import ... (the full path) would still work because the file tile16.py exists on disk. Python doesn't enforce privacy for module
files. Renaming to _tile16.py would make the "don't use this" convention stronger, but it's just a convention — both are still importable.
Given that everything inside the module is already prefixed with _ (_make_tile16x16, _TILE, _Tile16x16, etc.), I'd say removing from all/getattr is sufficient.
Renaming the file would require updating imports across ~5 files on every branch — a lot of churn for a convention signal.
Want to just remove it from the public surface, or also rename the file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

rename the file

The module is internal — all its symbols are already underscore-prefixed.
Remove from __all__ and __getattr__ in simt/__init__.py so it's not
accessible via qd.simt.tile16.
Comment thread python/quadrants/lang/simt/_tile16.py Outdated
@@ -0,0 +1,317 @@
# type: ignore

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why is this on type: ignore?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opus:

Every file in quadrants/lang/simt/ has it. It's a convention for this package — these modules define GPU kernel functions using @qd.func/@qd.dataclass with type annotations
like dtype (a runtime variable) as field types, which don't make sense to mypy/pyright. The # type: ignore suppresses type checker errors across the whole file.
It was there in the original branch; not something we added.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

no, it's not a convention. remove it please, and make sure pyright passes

Replace with targeted pyright suppressions: reportInvalidTypeForm=false
at file level for DSL type annotations, and individual
reportAttributeAccessIssue ignores for StructType monkey-patching.
Comment thread python/quadrants/lang/simt/_tile16.py
Comment thread python/quadrants/lang/simt/_tile16.py Outdated
self.r15 = arr[row, col_start + 15]

@qd.func
def _load3d(self, arr: qd.template(), i0, row_start, col_start, col_end, row_end):

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why i0? why not just i?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Opus:

No good reason — it was named to match the old row0/col0 convention. Now that those are row_start/col_start, i0 is inconsistent. Should I rename it to batch?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, good

Comment thread python/quadrants/lang/simt/_tile16.py Outdated
Each thread stores to arr[row_start + tid, col_start:col_end].
Threads where row_start + tid >= row_end skip the store.
"""
row = row_start + qd.i32(qd.simt.subgroup.invocation_id())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why do we need qd.i32?

}

void visit(InternalFuncStmt *stmt) override {
// TODO: support return type specification

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why is it ok to remove this comment?

@hughperkins

hughperkins commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator Author

Some queitons to opus:

question: is there any code that is duplicate by or in this PR that we could refactorize better somehow?

Looking at _tile16.py on hp/tiles-1, the major duplication is:

  1. _load, _load3d, _store, _store3d — four methods with near-identical structure. Each has 16 guarded field accesses. They differ only in load-vs-store direction and
    2D-vs-3D indexing. That's ~4 x 48 = ~192 lines of boilerplate.
  2. _eye_ — 16 lines zeroing + 16 conditional lines setting diagonal = 32 repetitive lines.

The question is what can actually be refactored given the Quadrants DSL constraints:
• The 16 per-register lines within a method: Can't be looped because self.r0..self.r15 are individual named fields, not an array. The Quadrants compiler processes the AST and
needs to see each field access explicitly.
_load3d in terms of _load: If Quadrants supported 2D views of 3D arrays (arr[batch, :, :]), then _load3d would just call _load. Does it?
_load vs _store: These are inverses of each other (self.r0 = arr[...] vs arr[...] = self.r0). Hard to unify without higher-order functions in the DSL.
• Code generation: We could generate all four methods programmatically at the Python level (before qd.dataclass sees them), collapsing ~200 lines of source into ~20 lines of
generation logic. The generated code would be identical, but the source wouldn't be repetitive.

Want me to try the code generation approach? Or check if 3D-to-2D slicing works first?

Is there any code or code change in this PR that might be embarrassing in some way if published in a public github repo?

A few things that would catch a reviewer's eye:

  1. col_start + 0 — appears 4 times. It's col_start. The + 0 is there for visual alignment with + 1, + 2, etc., but it looks like the author doesn't know that adding zero is a
    no-op.
  2. qd.i32(qd.simt.subgroup.invocation_id()) — as we just discussed, invocation_id() already returns i32. The redundant cast suggests the author didn't check the return type.
  3. Test variables named col_stop/row_stop while the API parameters are col_end/row_end — inconsistent naming between the API and its tests.
  4. dst_row_dx/dst_col_dx — unusual naming. row_offset/col_offset would be clearer.

No secrets, offensive content, or anything truly bad. Want me to clean these up?

yes please

⬢ To-do All done
☒ Remove col_start + 0 → col_start in _tile16.py
☒ Remove redundant qd.i32() casts in _tile16.py
☒ Rename col_stop/row_stop → col_end/row_end in tests
☒ Rename dst_row_dx/dst_col_dx → row_offset/col_offset in tests
☒ Run tests, commit, push, propagate

Done. All cleaned up and propagated:
• Removed col_start + 0 → just col_start
• Removed unnecessary qd.i32() casts
• Renamed dst_row_dx/dst_col_dx → row_offset/col_offset
• Renamed col_stop/row_stop → col_end/row_end to match the API

Are there any layout inconsistencies in this code relative to itself and to other existing repo code?

Here's what I found:

  1. Section separator banner (# ====...====) — only one other file in the entire python/quadrants/ tree uses these (primitive_types.py), and it uses shorter ones. The other simt
    files (subgroup.py, block.py, warp.py) don't use any. Feels out of place.
  2. Unicode characters — em dashes (—) and en dashes (–) in the docstrings/comments (lines 10, 22, 31). The rest of the codebase almost certainly uses plain ASCII (-- or -).
  3. Duplicate _skip_if_f64_unsupported — defined in both test_tile16.py and test_simt.py with slightly different behavior (ours skips Vulkan unconditionally, test_simt.py only
    skips Vulkan on macOS). Duplication and inconsistent skip logic.
  4. No blank line between # pyright: directive and the module docstring on line 2. Minor but looks cramped.

Want me to fix these — remove the banner, replace Unicode with ASCII, add the blank line, and factor out the f64 skip helper?

yes

⬢ To-do All done
☒ Remove section separator banner in _tile16.py
☒ Replace Unicode dashes with ASCII in _tile16.py
☒ Add blank line after pyright directive
☒ Factor out _skip_if_f64_unsupported to test_utils or shared location
☒ Run tests, commit, push, propagate

Done. All branches pushed with:
• Section banner removed
• Unicode dashes replaced with --
• Blank line added after pyright directive
• skip_if_f64_unsupported moved to test_utils and reused from tests

Are there any coding inconsistencies in this code relative to itself and to other existing repo code?

Here's what I found. The main coding inconsistency is:
Decorator ordering: In test_tile16.py, @test_utils.test is placed before @pytest.mark.parametrize:

@test_utils.test(arch=qd.gpu)
@pytest.mark.parametrize("qd_dtype", _QD_DTYPES)
def test_tile16_zeros(qd_dtype):

But 94.5% of tests in the repo (189 out of 200 instances across 61+ files) use the opposite order -- @pytest.mark.parametrize first, then @test_utils.test:

@pytest.mark.parametrize("qd_dtype", _QD_DTYPES)
@test_utils.test(arch=qd.gpu)
def test_tile16_zeros(qd_dtype):

This affects every parametrized test in test_tile16.py on hp/tiles-1 (and carries through to all downstream branches).
This same inconsistency is also present in hp/tiles-4's expanded test file, along with a few more issues that we already fixed in tiles-1 but haven't propagated yet
(_skip_if_f64_unsupported still local, dst_row_dx naming, section banners, M without underscore prefix).
Want me to fix the decorator ordering on hp/tiles-1 and propagate?

yes

⬢ To-do All done
☒ Fix decorator ordering in hp/tiles-1 test_tile16.py
☒ Commit and push hp/tiles-1
☒ Propagate fix through hp/tiles-2a, 2b, 3, 4, 5

Done. All branches updated and pushed:
• hp/tiles-1: Swapped @test_utils.test / @pytest.mark.parametrize ordering to match the repo convention (parametrize first, then test_utils.test).
• hp/tiles-2a, 2b, 3: Clean rebases, no conflicts.
• hp/tiles-4: Resolved conflict in the full test file replacement, and also fixed the same set of inconsistencies that were already fixed in tiles-1 (local
_skip_if_f64_unsupported -> test_utils.skip_if_f64_unsupported, dst_row_dx -> row_offset, M -> _M, removed section banners, removed unnecessary qd.i32() casts, replaced
Unicode dashes with ASCII).
• hp/tiles-5: Clean rebase on top of tiles-4.

All branches pass pre-commit.

Is there anything in this PR that could be implemented in less code?

The obvious candidate is the massive repetition: _load, _load3d, _store, _store3d, and eye each repeat the same pattern 16 times for r0 through r15. Together that's ~230
lines of the 312-line file.
There are two angles:

  1. Can _load3d/_store3d delegate to _load/_store?
    The 3D variants are identical to the 2D ones except arr[batch, row, col] vs arr[row, col] and arr.shape[2] vs arr.shape[1]. If the DSL supported something like arr[batch]
    returning a 2D view, _load3d would be a one-liner. But compiled GPU DSLs typically don't support that kind of view construction.
  2. Can the 16-line register repetition be looped?
    The DSL has qd.static(range(16)) for compile-time unrolling, but even with a static loop variable i, you can't write self.r{i} -- field access is resolved by name at AST parse
    time, not by string interpolation.
    You could code-generate the method bodies in Python at class-definition time (using exec() or AST construction), turning 16 hand-written lines into a 2-line loop. But that
    trades verbosity for readability -- the generated code would be harder to debug and inspect.
    So the bottom line: the repetition is an inherent consequence of having 16 individually-named scalar register fields in a DSL that doesn't support computed field access. It's
    verbose but intentional -- every similar register-tile implementation I've seen (WMMA wrappers, etc.) has this same pattern. There's no easy way to cut it down without
    sacrificing readability or requiring DSL changes.

- Remove redundant col_start + 0 (just col_start)
- Remove unnecessary qd.i32() casts (invocation_id already returns i32)
- Rename dst_row_dx/dst_col_dx to row_offset/col_offset
- Rename col_stop/row_stop to col_end/row_end to match API naming
- Remove section separator banner (not used elsewhere in simt/)
- Replace Unicode dashes with ASCII
- Add blank line after pyright directive
- Factor out skip_if_f64_unsupported into test_utils
parametrize decorators go before @test_utils.test, matching 94% of
existing tests in the codebase.
@hughperkins

hughperkins commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator Author

(continuing in new comment, since previous comment getting unwiedly)

Is there anything in this PR that could be implemented in a way that will run faster at runtime?

The runtime-critical code is in _tile16.py. Let me look at the _load/_store methods specifically.
The hot path is the 16 per-element bounds checks in _load and _store:

/home/hugh/ais/seagull/quadrants/python/quadrants/lang/simt/_tile16.py lines 62-74

      @qd.func
      def _load(self, arr: qd.template(), row_start, col_start, col_end, row_end):
          # ...
          row = row_start + qd.simt.subgroup.invocation_id()
          if row < row_end:
              arr_col_end = arr.shape[1]
              if arr_col_end < col_end:
                  col_end = arr_col_end
              if col_start < col_end:
                  self.r0 = arr[row, col_start]
              if col_start + 1 < col_end:
                  self.r1 = arr[row, col_start + 1]
              # ... 14 more branches ...

Every load/store does 16 individual if col_start + i < col_end checks. For the common case (full 16-column tile load), all 16 checks pass and they're pure overhead.
A fast path could skip them entirely:

if col_start + 16 <= col_end:
# Full tile: no per-element checks needed
self.r0 = arr[row, col_start]
self.r1 = arr[row, col_start + 1]
...
self.r15 = arr[row, col_start + 15]
else:
# Partial: check each element
if col_start < col_end:
self.r0 = arr[row, col_start]
...

That said, in practice this is likely a marginal win: col_end is typically uniform across the subgroup, so the GPU handles these as predicated instructions rather than true
divergent branches. The compiler may already be optimizing this well.
Everything else looks fine -- invocation_id() is called once per method, no redundant memory accesses, no unnecessary synchronization. The memory access pattern (each thread
reads its own row) is inherent to the row-per-thread design and can't be changed without redesigning the tile layout.
Short answer: the only candidate is adding a fast path for full-tile loads/stores, but it doubles the code size and the GPU compiler likely already handles the uniform-branch
case efficiently. Probably not worth it unless profiling shows it's a bottleneck.

Is there anything in this PR which might exhibit incorrect behavior in some way?

  Yes, there's an asymmetry in bounds checking that could lead to incorrect behavior:
  Column bounds are clamped to the array shape, but row bounds are not.
  In _load:

   /home/hugh/ais/seagull/quadrants/python/quadrants/lang/simt/_tile16.py lines 68-72

              row = row_start + qd.simt.subgroup.invocation_id()
              if row < row_end:
                  arr_col_end = arr.shape[1]
                  if arr_col_end < col_end:
                      col_end = arr_col_end

  If a caller passes row_end > arr.shape[0], and a thread's row_start + tid is < row_end but >= arr.shape[0], it reads arr[row, ...] out of bounds. On a GPU this is undefined
  behavior -- could return garbage or crash.
  The column dimension is explicitly clamped to arr.shape[1] (line 70-72), but no equivalent clamping exists for rows:

  # Columns: clamped (safe)
  arr_col_end = arr.shape[1]
  if arr_col_end < col_end:
      col_end = arr_col_end
  # Rows: NOT clamped (caller must ensure row_end <= arr.shape[0])

  The same asymmetry exists in _store, _load3d, and _store3d.
  In practice this is mitigated because:
  • PR 3+ introduces slice syntax that constructs bounds from the array shape
  • The tests in PR 1 use correctly-sized arrays

  But the raw _load/_store API has this inconsistency. Either rows should also be clamped (matching the column behavior), or columns should not be clamped (matching the row
  behavior and making the caller fully responsible). The current halfway approach is the most dangerous because it gives a false sense of safety.
  Want me to add row clamping to match the column clamping?

yes, and also unit tests for this

⬢ To-do All done
☒ Add row clamping to _load, _store, _load3d, _store3d in _tile16.py
☒ Add unit tests for row clamping (load/store beyond array rows)
☒ Commit, push, propagate through all branches

Done. Here's a summary of the changes:
_tile16.py -- Added row clamping to all four methods, symmetric with the existing column clamping:
• _load: arr_row_end = arr.shape[0]; if arr_row_end < row_end: row_end = arr_row_end
• _store: same pattern with arr.shape[0]
• _load3d: same pattern with arr.shape[1] (row dim in 3D)
• _store3d: same pattern with arr.shape[1]

test_tile16.py (hp/tiles-1) -- Added 4 new tests:
• test_tile16_load_clamp_to_array_rows -- loads from a 10-row array with row_end=16, verifies rows 10-15 are zero
• test_tile16_store_clamp_to_array_rows -- stores to a 10-row array with row_end=16, verifies no OOB write
• test_tile16_load3d_clamp_to_array_rows -- same for 3D
• test_tile16_store3d_clamp_to_array_rows -- same for 3D

test_tile16.py (hp/tiles-4) -- Added 2 proxy-API equivalents:
• test_tile16_load_clamp_to_array_rows
• test_tile16_store_clamp_to_array_rows

All branches rebased, pre-commit passes, force-pushed.

Column bounds were already clamped to arr.shape but row bounds were
not, which could cause out-of-bounds access when row_end exceeds the
array height. Add symmetric row clamping and tests for the new
behavior.
Change from (row_start, col_start, col_end, row_end) to
(row_start, row_end, col_start, col_end) so row and column
parameters are grouped together, matching numpy slice convention.
@hughperkins

hughperkins commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator Author

check the test coverage of the tests. Is there any code that is being added or modified in this PR, that is not being exercised by the tests? Is there any functionality that is implied by consistency or otherwise that is not being tested by these tests?

Here's the coverage analysis:
Code added but not exercised by any test:

  1. result.zeros = result (line 315) -- The .zeros factory alias is never used. Every test constructs tiles with Tile(). No test calls Tile.zeros().
  2. result.SIZE = _TILE (line 313) -- No test checks that Tile.SIZE == 16. It's set but never read.
  3. result._quadrants_internal = True (line 314) -- This flag tells the AST purity checker to exempt tile code from global-access restrictions. Not testable in PR 1 since the
    purity exemption logic comes in PR 3.
  4. type_check.cpp change -- The fix to InternalFuncStmt preserves polymorphic return types for operations like subgroupShuffle. But PR 1's tests never trigger a shuffle. The
    fix is needed for later PRs (Cholesky uses shuffles on f32 values), so it's dead code from PR 1's perspective.

Functionality implied by consistency but not tested:
5. Column clamping to array shape -- Row clamping is tested (10-row array, row_end=16, verifying extra rows are zero). But there's no equivalent test for a narrow array --
loading from a (16, 10) array with col_end=16 and verifying that columns 10-15 stay zero. The column clamping code exists (lines 73-75) but only gets exercised for the
narrow-array case in PR 4.
6. Store partial columns, verify untouched columns -- test_tile16_load_store with ncols=5 loads 5 columns, then stores 5 columns. But the destination is pre-filled with -1.0
and the assertion checks the whole grid, so it implicitly verifies untouched columns stay -1.0. However, there's no test that loads a full 16 columns and then stores only a
subset (which would verify that _store's column bounds independently clamp, separate from _load's).
7. _eye_() on a zero tile -- test_tile16_eye_inplace loads non-zero data then calls eye(), verifying the overwrite. But Tile.eye() calls eye() on a freshly zeroed tile.
There's no test that verifies eye() works correctly on a zero tile specifically (though test_tile16_eye implicitly tests this via Tile.eye()).
8. _make_tile16x16 caching -- The cache is exercised (multiple tests call _make_tile16x16(qd.f32), hitting the cache on subsequent calls), but no test verifies that the
cached object is the same instance, i.e., _make_tile16x16(qd.f32) is _make_tile16x16(qd.f32).

Most impactful gaps to close would be items 1 and 5:
• A one-line test for Tile.zeros() (since it's an alias exposed by the API)
• A column-clamping test mirroring the existing row-clamping test

Want me to add these?

Request to Opus: " │ → - remove result._quadrants_internal = True
│ - add test for Tile.SIZE == 16 │
│ - add test for Tile.zeros() │
│ - move ``type_check.cpp change to a later PR (or maybe a new PR, lke '1b' perhaps) │
│ - add test for 'Column clamping to array shape" │
│ - add check for 'Store partial columns, verify untouched columns' │
│ - add test for '`_make_tile16x16` caching' │
│ │
│ Make a todo list ofr htese, then go through one by one"

All done. Here's what was completed:

  1. Removed result._quadrants_internal = True from _tile16.py on hp/tiles-1 -- it's not consumed until PR 3 where ast_transformer.py checks it. Re-added it in hp/tiles-3 with
    a dedicated commit.
  2. Added test_tile16_size_constant -- asserts Tile.SIZE == 16.
  3. Added test_tile16_zeros_factory -- creates a tile via Tile.zeros(), stores it, and verifies all zeros (parametrized f32/f64).
  4. Moved type_check.cpp change to hp/tiles-1b -- new branch forked from hp/tiles-1 containing only the InternalFuncStmt fix. The chain is now: main -> hp/tiles-1 ->
    hp/tiles-1b -> hp/tiles-2a -> ...
  5. Added test_tile16_load_clamp_to_array_cols -- loads from a (16, 10) array into a 16x16 tile, verifies columns beyond width are zero (parametrized f32/f64).
  6. Added test_tile16_store_partial_cols_untouched -- loads full 16 columns, stores only 10, verifies remaining destination columns are untouched at -1.0 (parametrized
    f32/f64).
  7. Added test_tile16_make_caching -- verifies _make_tile16x16(f32) is _make_tile16x16(f32) and f32 is not f64.

…R 3); revert type_check.cpp (moving to PR 1b)

- Add tests: Tile.SIZE, Tile.zeros(), column clamping, partial-col store, _make_tile16x16 caching
- Remove _quadrants_internal flag (only consumed by ast_transformer in PR 3)
- Revert type_check.cpp to main (moving to separate PR 1b)
@hughperkins

hughperkins commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator Author

check the factorization of the tests. are there some tests that could be merged, e.g. making use of parametrize?

Here's my analysis of merge opportunities:

  1. test_tile16_zeros + test_tile16_zeros_factory (lines 13-30, 284-302)
    These are nearly identical — same kernel shape, same assertion (all zeros). The only difference is Tile() vs Tile.zeros(). However, since these are compiled GPU kernels, you
    can't easily parametrize the constructor call inside the kernel body. They'd need two separate kernels regardless. Verdict: marginal gain, reduced clarity.
  2. test_tile16_load_clamp_to_array_rows + test_tile16_store_clamp_to_array_rows (lines 127-177)
    These use the exact same kernel (_load then _store with full bounds). They differ only in which array is undersized (src vs dst) and in the assertions. Could be merged with a
    parametrize on (src_shape, dst_shape) and conditional assertions. Same for the 3D pair (lines 180-230). Verdict: best candidate — same kernel, 4 tests could become 2.
  3. test_tile16_load_clamp_to_array_cols + test_tile16_store_partial_cols_untouched (lines 305-357)
    These use different kernels (different col_end in _store), so they can't share a kernel. Verdict: not mergeable.
  4. test_tile16_eye + test_tile16_eye_inplace (lines 33-73)
    Different things: factory vs in-place mutation of a loaded tile. Verdict: not mergeable, testing distinct behavior.
    The only real win is merging the row-clamping pairs: collapse 4 tests into 2 by parametrizing on which side (src or dst) is short. That said, it would slightly reduce
    readability since the assertions differ per case. Want me to do it, or do you think the current factorization is acceptable?

reminder: you can use if qd.static(...) in quadrants kernels to switch between different compiled code.

Good point — that changes the picture. With qd.static(), more tests can share a single kernel via compile-time branching. Updated analysis:
Mergeable with qd.static:

  1. test_tile16_zeros + test_tile16_zeros_factory — parametrize use_zeros_alias, branch Tile() vs Tile.zeros() via qd.static
  2. test_tile16_eye + test_tile16_eye_inplace — parametrize inplace, branch Tile.eye() vs Tile() + _load + eye() via qd.static

Mergeable without qd.static (same kernel):
3. load_clamp_to_array_rows + store_clamp_to_array_rows — same kernel, parametrize on which side is short
4. load3d_clamp_to_array_rows + store3d_clamp_to_array_rows — same kernel, parametrize on which side is short

That would reduce 8 test functions → 4. Let me do it.

Done. Merged 8 test functions into 4 by:

  1. test_tile16_zeros + test_tile16_zeros_factory → single test_tile16_zeros with use_zeros_alias param, using qd.static to branch between Tile() and Tile.zeros()
  2. test_tile16_eye + test_tile16_eye_inplace → single test_tile16_eye with inplace param, using qd.static to branch between Tile.eye() and Tile() + _load + eye()
  3. test_tile16_load_clamp_to_array_rows + test_tile16_store_clamp_to_array_rows → single test_tile16_clamp_to_array_rows with clamp_side param (same kernel, different array
    shapes)
  4. test_tile16_load3d_clamp_to_array_rows + test_tile16_store3d_clamp_to_array_rows → single test_tile16_3d_clamp_to_array_rows with clamp_side param

Net: -72 lines. The column-clamping tests (load_clamp_to_array_cols and store_partial_cols_untouched) were left separate since they use different kernels (different col_end in
_store) and test conceptually different clamping mechanisms.

- zeros + zeros_factory -> single test with use_zeros_alias param
- eye + eye_inplace -> single test with inplace param (qd.static branch)
- 2D row clamp load + store -> single test with clamp_side param
- 3D row clamp load + store -> single test with clamp_side param
Variables defined inside qd.static branches don't escape to outer scope.
@hughperkins hughperkins marked this pull request as ready for review April 12, 2026 15:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2988a5b2e9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

arr_row_end = arr.shape[0]
if arr_row_end < row_end:
row_end = arr_row_end
row = row_start + qd.simt.subgroup.invocation_id()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive tile row from block-local thread id

Using subgroup.invocation_id() to choose the tile row assumes a subgroup width of at least 16, but this is not guaranteed (the repo’s SIMT tests only assume a minimum subgroup size of 4). On GPUs where a 16-thread block is split into smaller subgroups (e.g., 2×8 lanes), lane IDs repeat per subgroup, so _load/_store/_load3d/_store3d/_eye_ operate on rows 0–7 twice and never touch rows 8–15, corrupting tile I/O and identity initialization.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a great observation by Claude.

This is being addressed in #455

Comment thread tests/test_utils.py
Comment on lines +327 to +334
def skip_if_f64_unsupported(dtype):
"""Skip the current test if the active backend does not reliably support f64."""
if dtype != qd.f64:
return
arch = qd.lang.impl.current_cfg().arch
if arch == qd.metal:
pytest.skip("Metal does not support f64")
if arch == qd.vulkan:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The new shared skip_if_f64_unsupported() helper in test_utils.py skips Vulkan f64 on all platforms, but the pre-existing _skip_if_f64_unsupported() in test_simt.py only skips on macOS (where MoltenVK is the Vulkan implementation). This causes tile16 f64 tests to be silently skipped on Linux Vulkan even though Vulkan natively supports f64 there — a test coverage regression. Fix by adding and platform.system() == 'Darwin' to the Vulkan branch, matching test_simt.py's established convention.

Extended reasoning...

What the bug is

tests/test_utils.py lines 334-335 (the new shared helper) unconditionally skip all Vulkan f64 tests:

if arch == qd.vulkan:
    pytest.skip("Vulkan does not reliably support f64")

But the pre-existing _skip_if_f64_unsupported() in test_simt.py lines 19-20 only skips on Darwin:

if arch == qd.vulkan and platform.system() == 'Darwin':
    pytest.skip("MoltenVK does not support f64")

The rationale in test_simt.py is precise: the problem is MoltenVK (the macOS Vulkan implementation), which does not support f64. Native Vulkan on Linux has no such limitation.

The specific code path

Every tile16 f64 test in test_tile16.py calls test_utils.skip_if_f64_unsupported(qd_dtype) at the top. When the test runner is configured to use Vulkan on Linux (which conftest.py does), this call reaches the new helper, hits the unconditional arch == qd.vulkan branch, and issues pytest.skip — regardless of whether the platform is Linux, where f64 Vulkan actually works.

Why existing code doesn't prevent it

The PR author was aware of the discrepancy (PR timeline: "ours skips Vulkan unconditionally, test_simt.py only skips Vulkan on macOS") but chose to be more conservative. No justification is given for why tile16 operations would fail on Linux Vulkan when test_simt.py shuffle f64 tests (which exercise the same underlying subgroup intrinsics) continue to pass there.

Impact

All tile16 f64 tests (test_tile16_zeros, test_tile16_eye, test_tile16_load_store, test_tile16_clamp_to_array_rows, etc.) are silently skipped on Linux Vulkan. Meanwhile, test_simt.py f64 tests run normally on the same platform with the same backend. This is a silent test coverage regression — CI stays green while f64 tile16 coverage is absent on Linux Vulkan.

Step-by-step proof

  1. Linux CI runner with Vulkan enabled runs pytest.
  2. test_tile16_zeros[qd_dtype=qd.f64-arch=vulkan] is invoked.
  3. test_utils.skip_if_f64_unsupported(qd.f64) is called.
  4. dtype \!= qd.f64 is False, so execution continues.
  5. arch = qd.lang.impl.current_cfg().arch returns qd.vulkan.
  6. arch == qd.vulkan is True → pytest.skip("Vulkan does not reliably support f64") fires.
  7. Test is marked as skipped with no failure, no warning.
  8. The same runner executes test_simt.py f64 shuffle tests with Vulkan — those use _skip_if_f64_unsupported which checks platform.system() == 'Darwin' → False on Linux → tests run and pass.

How to fix

Change the Vulkan branch in test_utils.skip_if_f64_unsupported to match the convention established in test_simt.py:

if arch == qd.vulkan and platform.system() == 'Darwin':
    pytest.skip("MoltenVK does not support f64")

This restores f64 tile16 coverage on Linux Vulkan while still correctly skipping on macOS where MoltenVK is the implementation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is intentional. Some tests were failing wiht f64, even on linux vulkan.

@erizmr erizmr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving on the basis that I have reviewed the design and the public facing API, and they look reasonable to me.

@hughperkins hughperkins enabled auto-merge (squash) April 12, 2026 16:31
@hughperkins hughperkins merged commit dc4ef27 into main Apr 12, 2026
52 of 53 checks passed
@hughperkins hughperkins deleted the hp/tiles-1 branch April 12, 2026 17:08
npoulad1 added a commit to ROCm/quadrants that referenced this pull request Jun 8, 2026
* [Misc] Warn user to disable caching when print_ir/QD_DUMP_IR enabled (Genesis-Embodied-AI#425)

Co-authored-by: v01dxyz <v01dxyz@v01d.xyz>

* [Build] Pin torch version to CUDA 12.8 for CUDA tests (Genesis-Embodied-AI#428)

* [Misc] Fixing up taichi-dev urls (Genesis-Embodied-AI#429)

* [Perf] Rename cuda_graph to gpu_graph across the codebase (Genesis-Embodied-AI#430)

* Misc: fix typo integeral -> integral (Genesis-Embodied-AI#434)

Co-authored-by: v01dxyz <v01dxyz@v01d.xyz>

* [Perf] CUDA graph 4: call from multiple locations (Genesis-Embodied-AI#420)

* [Bug] Fix fastcache not restoring graph_do_while_arg (Genesis-Embodied-AI#435)

* [Perf] Cache last-call result in perf_dispatch for single-compatible case (Genesis-Embodied-AI#438)

* Fix gpu_graph fallback on old Nvidia GPU. (Genesis-Embodied-AI#443)

* Fix shared memory offset not reset between CUDA kernels. (Genesis-Embodied-AI#442)

* [Misc] Allow disabling GPU graph via QD_GPU_GRAPH=0 env var (Genesis-Embodied-AI#439)

* [Misc] Add named top-level loops (Genesis-Embodied-AI#440)

* [Misc] Rename gpu_graph to graph (Genesis-Embodied-AI#446)

* [Misc] Add cross-platform shuffle (Genesis-Embodied-AI#447)

* [Bug] Fix graph_do_while on Windows: search for cudadevrt.lib (Genesis-Embodied-AI#456)

* [Bug] Also search default CUDA toolkit install location on Windows (Genesis-Embodied-AI#461)

* [SPIRV] Feature Parity Atomics & Shared Array (Genesis-Embodied-AI#432)

* [Misc] Change clang format to 120 characters (Genesis-Embodied-AI#463)

* [Misc] CUDA graph 5 Add fatbin (Genesis-Embodied-AI#464)

* [Bug] Reuse VkInstance across init/reset cycles (Genesis-Embodied-AI#465)

* [Perf] Tiles 1: _load, _store, _eye_ (Genesis-Embodied-AI#466)

* [Misc] Remove dead InternalFuncStmt type_check override (Genesis-Embodied-AI#471)

* [Perf] Tiles 2: add cholesky and ger (Genesis-Embodied-AI#472)

* [Perf] Tiles 2b: add triangular solve (Genesis-Embodied-AI#474)

* [Misc] Refactor: use _get_col/_set_col in tiles load/store/init (Genesis-Embodied-AI#475)

* [Build] Fix flaky test_clock_accuracy (Genesis-Embodied-AI#436)

* Fix AARCH64 emitting invalid asm in CUDA kernels. (Genesis-Embodied-AI#473)

Co-authored-by: Hugh Perkins <hughperkins@gmail.com>

* [AMDGPU] Enable HIP memory pool and surface pool-exhaustion errors. (Genesis-Embodied-AI#485)

* [AMDGPU] Scope hsaco tmp dir per-user to avoid collisions. (Genesis-Embodied-AI#484)

* [Perf] Tiles 3: Add slice syntax, qd.outer() and initial doc (Genesis-Embodied-AI#477)

* [AMDGPU] Fix gradient computation. (Genesis-Embodied-AI#486)

* Enable all backends that are supported in unit tests. (Genesis-Embodied-AI#488)

* Fix SPIRV ID overflow for large kernels due to autodiff. (Genesis-Embodied-AI#489)

* [Misc] Fix purity checker to allow accessing constants from quadrants modules (Genesis-Embodied-AI#487)

* [Misc] Increase tolerance for clock monotonic test (Genesis-Embodied-AI#492)

* [CI] Serialize api doc workflow (Genesis-Embodied-AI#494)

* [CI] Increase tolerance for clock test (Genesis-Embodied-AI#506)

* [CI] Increase clock test tolerance to 20% (Genesis-Embodied-AI#509)

* [Perf] Add tensor_type parametrization to tile16 tests (Genesis-Embodied-AI#504)

* [Perf] Tiles 4b: Migrate tiles16 tests to enable fastcache (Genesis-Embodied-AI#505)

* [Perf] Tiles 4c: add Tiles16x16 proxy (Genesis-Embodied-AI#507)

* [Perf] Tiles 4d: Consolidate slice error tests using parametrize (Genesis-Embodied-AI#508)

* [Perf] Tiles 4: add SharedArray slice support (Genesis-Embodied-AI#482)

* [Perf] Tiles 5: add Cholesky benchmark demo (Genesis-Embodied-AI#483)

* [Doc] Add user guide page for subgroup shuffle (Genesis-Embodied-AI#512)

* [Perf] Implement cross-platform shuffle_down (Genesis-Embodied-AI#510)

* [Perf] Add portable subgroup reduce_add and reduce_all_add (Genesis-Embodied-AI#511)

* [Perf] Add first warmup config to perf dispatch (Genesis-Embodied-AI#422)

* [AutoDiff] Autodiff 1: Add baseline adstack regression test for unary_collections (Genesis-Embodied-AI#500)

* [AutoDiff] Autodiff 2: Implement derivative for tan (Genesis-Embodied-AI#501)

* [AutoDiff] Autodiff 3: Recompute tanh/exp on the operand in the reverse pass (Genesis-Embodied-AI#502)

* [AutoDiff] Autodiff 4: Mark rsqrt as non-linear for adstack promotion (Genesis-Embodied-AI#503)

* [AutoDiff] Autodiff 5: Fix adjoint-alloca placement for GlobalLoads outside the current range-for (Genesis-Embodied-AI#496)

* [AutoDiff] Autodiff 6: Adstack regression tests (Genesis-Embodied-AI#491)

* [AutoDiff] Autodiff 7: Fix header size in AdStackAllocaStmt to match u64 runtime layout (Genesis-Embodied-AI#534)

* [AutoDiff] Autodiff 8: Surface LLVM adstack push/pop overflow as a Python exception (Genesis-Embodied-AI#535)

* [AutoDiff] Autodiff 9: Guard against LLVM worker-thread stack overflow from large per-task adstack budget (Genesis-Embodied-AI#495)

* [AutoDiff] Autodiff 10: Implement adstack for SPIR-V (Genesis-Embodied-AI#490)

* [AutoDiff] Autodiff 11: Latent adstack-adjacent fixes (AMDGPU hipFree, flush() keeps ctx_buffers_, always-preallocate) (Genesis-Embodied-AI#536)

* [Doc] Add AGENTS.md with instructions for AI agents (Genesis-Embodied-AI#541)

* [Bug] Abort kernel execution on assertion failure instead of segfaulting (Genesis-Embodied-AI#419)

* [Type] ndarray typing 1: Add eval_str=True to inspect.signature() calls (Genesis-Embodied-AI#411)

* [CI] Suppress reportPrivateImportUsage in torch-using files (Genesis-Embodied-AI#552)

* [Misc] QD_DUMP_IR dumps to files with the task_id added to the filename (Genesis-Embodied-AI#441)

* [Type] ndarray typing 2: Fix NDArray single-arg subscript crash (Genesis-Embodied-AI#412)

* [Test] Flush xdist channel before worker exit so test failure reports are visible (Genesis-Embodied-AI#555)

* [CI] Reduce test retries on CI from 3 to 1. (Genesis-Embodied-AI#554)

* [AutoDiff] Autodiff 12: Heap-backed adstack on LLVM backends (CPU/CUDA/AMDGPU) (Genesis-Embodied-AI#537)

* [AutoDiff] Autodiff 13: Heap-backed adstack on SPIR-V backends (Metal, Vulkan) (Genesis-Embodied-AI#493)

* [AutoDiff] Autodiff 14: Resolve bounded-inner-loop adstacks without default_ad_stack_size fallback (Genesis-Embodied-AI#539)

* [SPIRV] Vulkan SPIR-V correctness: atomic-view aliasing, PSB stride, narrow storage caps, u1 cast, per-init layer recheck (Genesis-Embodied-AI#513)

* [Build] Autodiff 15: Replace 2022 MoltenVK pin with LunarG Vulkan SDK fetch and sanitise MoltenVK cap advertisement (Genesis-Embodied-AI#551)

* [Test] Suppress stock pytest-timeout to avoid conflict with pytest_hardtle (Genesis-Embodied-AI#557)

* [Vulkan] Use SDK validation layer for debugPrintf instead of apt package (Genesis-Embodied-AI#562)

* [Test] Fix flaky perf_dispatch tests by increasing work amounts (Genesis-Embodied-AI#559)

* [Test] Add --maxfail CLI option to run_tests.py (default 20) (Genesis-Embodied-AI#558)

* [CI] Vulkan debug printf fix to address flaky tests (Genesis-Embodied-AI#563)

* [Docs] Add a new page to help for first time contributors (Genesis-Embodied-AI#426)

Authored-by: v01dxyz <v01dxyz@v01d.xyz>

* [AutoDiff] Autodiff 16: Resolve reverse-mode adstack depths per-launch via runtime-evaluated SizeExpr (Genesis-Embodied-AI#543)

* Fix: raise error if device memory allocation fails (Genesis-Embodied-AI#451) (Genesis-Embodied-AI#453)

Co-authored-by: v01dxyz <v01dxyz@v01d.xyz>
Co-authored-by: Hugh Perkins <hughperkins@gmail.com>

* [CI] Add CI job to check line wrapping of comments and docs (Genesis-Embodied-AI#564)

* [Misc] Add coverage report to PRs, including kernels (Genesis-Embodied-AI#470)

* [CI] CI wrap check feeds only diffs to agent (Genesis-Embodied-AI#567)

* Skip 'flaky' test on MacOS CI. (Genesis-Embodied-AI#573)

* [Test] Fix missing `import sys` in test_fail_device_memory_allocation (Genesis-Embodied-AI#574)

* [CI] Fix Vulkan debugPrintf flake with session-scoped warmup (Genesis-Embodied-AI#571)

* [AutoDiff] determine_ad_stack_size: replace whole-CFG Bellman-Ford with SCC + DAG DP (Genesis-Embodied-AI#575)

* [Test] Fix macOS OOM skip reason to describe actual root cause (Genesis-Embodied-AI#576)

* [Lang] whole_kernel_cse: 2.5x compile time speedup on large kernels (Genesis-Embodied-AI#577)

* [CI] Add CI check for unnecessarily deleted comments (Genesis-Embodied-AI#570)

* [CI] Migrate coverage report to github Check page (Genesis-Embodied-AI#566)

* [Lang] Skip IR verifier between passes unless debug=true (Genesis-Embodied-AI#579)

* [Lang] Inline AdStack ops on release LLVM codegen: dramatically reduces compile time for adstack-enabled reverse-mode kernels (Genesis-Embodied-AI#584)

* [CUDA] Honor offline_cache=False end-to-end so QD_OFFLINE_CACHE=0 actually gives a cold compile (Genesis-Embodied-AI#580)

* [Type] Tensor 24 (Genesis-Embodied-AI#561)

Co-authored-by: hugh <hugh@slurm-login-0.slurm-login.tenant-slurm.svc.cluster.local>

* [Lang] auto_diff host-walk reductions: dramatically faster front-end compile time on adstack-enabled reverse-mode kernels (Genesis-Embodied-AI#587)

* [AutoDiff] Speed up reverse-mode kernel launches on GPU backends (Genesis-Embodied-AI#578)

* [Vulkan] Move adstack-sizer scratch out of Function-scope memory to fix SPIR-V pipeline build failures (Genesis-Embodied-AI#588)

* [AutoDiff] Improve diagnosis of unsupported reverse-mode AD patterns (Genesis-Embodied-AI#590)

* [Bug] Fix: promote Ndarray to AnyArray in build_Name for flattened struct fields (Genesis-Embodied-AI#592)

* [SPIR-V] Shrink reverse-grad kernel MSL by ~50% (Genesis-Embodied-AI#591)

* [CI] Add CI check that PR changes have test coverage (Genesis-Embodied-AI#596)

* [Perf] Enable zero-copy in to_torch() and to_numpy() (Genesis-Embodied-AI#450)

* Add BufferView: safe sub-range ndarray access for kernels (Genesis-Embodied-AI#585)

Co-authored-by: alanray-tech <alanray-tech@users.noreply.github.com>
Co-authored-by: Hugh Perkins <hughperkins@gmail.com>

* [Doc] Add user-facing fastcache documentation (Genesis-Embodied-AI#597)

Co-authored-by: hugh <hugh@slurm-login-0.slurm-login.tenant-slurm.svc.cluster.local>

* [Misc] Upgrade to enable v1 dlpack so to_numpy(copy=False) writable (Genesis-Embodied-AI#598)

Co-authored-by: root <root@rtx-209-201.slurm-compute.tenant-slurm.svc.cluster.local>

* [AutoDiff] Cut reverse-mode adstack memory usage 10x on all backends (Genesis-Embodied-AI#599)

* [Misc] Add CI check for feature file factorization (Genesis-Embodied-AI#606)

* [Perf] Skip _recursive_set_args for all-Field frozen dataclass structs (Genesis-Embodied-AI#607)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [AutoDiff] SNode-arm bound-expr capture rejects fold-attack gate indices (Genesis-Embodied-AI#610)

* [Misc] Suppress field fastcache warning for qd.Tensor (Genesis-Embodied-AI#615)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [AutoDiff] Adstack heap: clip reducer count by per-task loop trip count (compile-time and SizeExpr-evaluated) (Genesis-Embodied-AI#611)

* [Misc] Forward copy= through qd.Tensor, add copy=None option (Genesis-Embodied-AI#616)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [Doc] Update README (Genesis-Embodied-AI#617)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [CI] Fix coverage report showing def lines as uncovered (Genesis-Embodied-AI#623)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [Perf] Generic launcher: persistent context, JIT-pointer reuse, Metal compute encoder, LLVM-GPU async memory ops (Part 1/2) (Genesis-Embodied-AI#619)

* [CI] Encode Python-first testing policy in coverage-check prompt (Genesis-Embodied-AI#622)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [CI] Add PR Line change report (Genesis-Embodied-AI#624)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [CI] Disable quadrants pytest plugin during quadrants internal coverage runs (Genesis-Embodied-AI#629)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [AutoDiff] Adstack load+store eliminations: EliminateRecomputableAdStackPushes pass + leaf extensions (Genesis-Embodied-AI#621)

* [CI] Simplify coverage PR comment to a single linked line (Genesis-Embodied-AI#630)

* [CUDA] Add AGX Thor, SM_110 (Genesis-Embodied-AI#631)

Co-authored-by: Johnny Nunez and Hugh Perkins

* [CI] Lines changed report: collapse PR comment to a single linked totals line (Genesis-Embodied-AI#632)

* [FEATURE] Support external Metal command queue via qd.init (Genesis-Embodied-AI#618)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [Perf] Cache adstack-sizer metadata per task across SPIR-V + LLVM-GPU; per-snode / DeviceAllocation invalidation (Part 2/2) (Genesis-Embodied-AI#620)

* [AutoDiff] Disable EliminateRecomputableAdStackPushes pending mutated-SNode chain-leaf fix (Genesis-Embodied-AI#633)

* [AutoDiff] Adstack chain-clone safety: mutated-SNode leaf reject + load_top consumer-aware guard (Genesis-Embodied-AI#634)

* [Docs] Add user-guide page for qd.simt.block.* primitives (Genesis-Embodied-AI#638)

* [Docs] Expand qd.simt.subgroup user-guide page to cover every op (Genesis-Embodied-AI#639)

* [Perf] Streams 1-4 (Genesis-Embodied-AI#410)

* [Docs] Add user-guide page for matrix decompositions and solvers (Genesis-Embodied-AI#643)

* [Bug] Revert "[Perf] Streams 1-4 (Genesis-Embodied-AI#410)" (Genesis-Embodied-AI#650)

* [Docs] Add user-guide page for atomics and bit operations (Genesis-Embodied-AI#640)

* [Docs] Add user-guide page for qd.simt.grid.* primitives (Genesis-Embodied-AI#641)

* [AutoDiff] Adstack max-reducer: parallel multi-axis MaxOverRange dispatch (Genesis-Embodied-AI#635)

* [AMDGPU] Fix amdgpu parallel rand init (Genesis-Embodied-AI#658)

* [Perf] Adstack: skip max-reducer recognizer on CPU + lift host-eval cap (Genesis-Embodied-AI#655)

* [Perf] Re-land Streams 1-4 with bug fixes (Genesis-Embodied-AI#653)

* [AMDGPU] Apply device_memory_GB=0.3 cap to AMDGPU tests (Genesis-Embodied-AI#659)

* [Perf] Per-launch host sync: drop wait_idle on SPIR-V, pin stream and drop stream_synchronize on CUDA/AMDGPU (Genesis-Embodied-AI#654)

* [AMDGPU] Unload hipModule_t in JITModuleAMDGPU destructor (Genesis-Embodied-AI#660)

* [AMDGPU] Trim default mempool on qd.reset() (Genesis-Embodied-AI#669)

* [AMDGPU] Hoist rand-state buffer to process lifetime (Genesis-Embodied-AI#668)

* [Streams] Use events for streams serialization on AMDGPU and CUDA (Genesis-Embodied-AI#667)

* [Perf] Adstack max-reducer: launch cache + zero-copy result map; content-stable registry_id (Genesis-Embodied-AI#671)

* [SPIR-V] dispatch_max_reducers: register each task with the real kernel name (Genesis-Embodied-AI#675)

* [AutoDiff] Debug-mode field/grad/dual: dtype, layout, and access-time invariants (Genesis-Embodied-AI#677)

* [Docs] Add user-guide page for qd.algorithms.* device-wide algorithms (Genesis-Embodied-AI#642)

Co-authored-by: alanray-tech <alan.ray@genesis-ai.company>

* [Docs] Doc for existing atomics: switch support table to per-backend columns (Genesis-Embodied-AI#657)

Co-authored-by: alanray-tech <alan.ray@genesis-ai.company>

* [GPU] Cross gpu atomics (Genesis-Embodied-AI#666)

Co-authored-by: alanray-tech <alan.ray@genesis-ai.company>

* [GPU] Make block operations portable cross-gpu (Genesis-Embodied-AI#664)

* [Perf] CPU LLVM adstack-cache: skip per-launch bump-writes + ndarray_shapes capture on forward-only handles (Genesis-Embodied-AI#685)

* [GPU] Cross-GPU for grid ops (Genesis-Embodied-AI#670)

* [Math] Make bitop operations portable cross-gpu (Genesis-Embodied-AI#662)

* [AMDGPU] Always use wave64, on both RDNA and CDNA (Genesis-Embodied-AI#687)

* [AMDGPU] Use syncscope("agent") for atomix xor to avoid CAS livelock (Genesis-Embodied-AI#672)

* [GPU] New bit ops for QIPC (Genesis-Embodied-AI#679)

* [GPU] Subgroup ops cross-gpu (Genesis-Embodied-AI#665)

* [Graph] Rename CUDA Graph to Graph in docs (Genesis-Embodied-AI#691)

* [SPIR-V] Fix FIFO-queue ordering when sharing command queue. (Genesis-Embodied-AI#694)

* [Atomics] New QIPC ops for atomics (Genesis-Embodied-AI#690)

* Pass dataclass sub-structs into qd.func (Genesis-Embodied-AI#698)

* [AMDGPU] HIP graph runtime support for @qd.kernel(graph=True) (Genesis-Embodied-AI#692)

* [CI] Add per-file timing report to Mac Metal test job (Genesis-Embodied-AI#695)

Co-authored-by: Cursor <cursoragent@cursor.com>

* [CI] Enable kernel disk cache during tests (Genesis-Embodied-AI#696)

* [Math] New QIPC ops for single-threaded linalg (Genesis-Embodied-AI#683)

* [BREAKING][GPU] New QIPC ops for subgroups (Genesis-Embodied-AI#676)

* [GPU] New QIPC ops for block (Genesis-Embodied-AI#684)

* [GPU] New device-level ops for QIPC (Genesis-Embodied-AI#693)

* [algorithms] PrefixSumExecutor: drop unused GRID_SZ local (Genesis-Embodied-AI#701)

* [block] sync(): fix unsupported-arch error message (Genesis-Embodied-AI#700)

* [volatile_load] add qd.volatile_load primitive (closes Genesis-Embodied-AI#648) (Genesis-Embodied-AI#702)

* [AutoDiff] Reject recycled identity_key in AdStackCache::register_adstack_sizing_info (Genesis-Embodied-AI#708)

* [Vulkan] Declare GroupNonUniform SPIR-V caps and enable shaderSubgroupExtendedTypes (Genesis-Embodied-AI#707)

* Fix duplicate HIP graph driver-function declarations after v1.0.0 merge

The amd-integration fork had cherry-picked the HIP graph driver functions
(graph_create / graph_destroy / graph_add_kernel_node / graph_instantiate /
graph_exec_destroy / graph_launch), and upstream v1.0.0 added the same set.
The per-file 3-way merge appended both copies into
amdgpu_driver_functions.inc.h, producing redeclaration errors that broke the
AMDGPU RHI/runtime compile. Drop the upstream duplicate block; the signatures
are identical to the fork's existing declarations.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix AMDGPU launcher coherence and num_instructions visibility after v1.0.0 merge

- kernel_launcher.cpp: the 3-way merge spliced upstream v1.0.0's launch_llvm_kernel
  rewrite (ephemeral arg/context buffers, explicit-stream path, AmdgpuDefaultStream
  PinGuard) onto the AMD fork's kernarg-by-value + persistent-scratch design,
  leaving references to undefined `ephemeral_context_ptr`. Restore the fork's
  coherent launch_llvm_kernel verbatim; it calls the (already merged) enhanced
  launch_offloaded_tasks, which keeps the max-reducer dispatch and stream-parallel
  groups adapted onto the AMD launch path.
- llvm_context.h: both the fork and upstream added `num_instructions`; the merge
  kept upstream's private placement, but the AMDGPU codegen force-inline heuristic
  calls it statically from outside the class. Move it back to the public section.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore async result D2H and hoist kernarg vectors in AMDGPU launcher

The v1.0.0 merge resolution regressed two amd-integration baseline
optimizations in launch_llvm_kernel / launch_offloaded_tasks:

  - The per-launch result-buffer copy was a blocking memcpy_device_to_host,
    forcing a host stall on every value-returning launch and serializing the
    GPU pipeline. Restore the async D2H (the caller synchronizes lazily when it
    needs the value); external-array transfers still stream_synchronize once
    before reading back.

  - launch_task constructed the kernarg std::vectors from initializer lists
    ({kernarg_payload} / {kernarg_size}) on every dispatch (heap alloc + free
    per launch). Hoist arg_ptrs/arg_sizes out of the per-task launch and reuse.

Co-authored-by: Cursor <cursoragent@cursor.com>

* amdgpu: default to LDS permlane64 emulation; drop host-x86 barrier asm on retarget

Two AMDGPU JIT-compile crashes surfaced after the v1.0.0 merge pulled in the QIPC subgroup
ops (Genesis-Embodied-AI#676), which made the rigid constraint solver's wave-cooperative reductions route through
`amdgpu_cross_half_shuffle_i32`. Both manifested as a SIGSEGV inside
`llvm::SIInstrInfo::getInstSizeInBytes` during `JITSessionAMDGPU::compile_module_to_hsaco`
(i.e. at first kernel launch), and reproduce on gfx942 / MI300X. Baseline 0.4.6 never emitted
these constructs, which is why it was unaffected.

1. Native `llvm.amdgcn.permlane64` lowering crashes the bundled LLVM 22.1.0 AMDGPU backend.
   Default `amdgpu_permlane64` to the existing LDS-roundtrip software emulation on every target
   (it produces identical results). Add `QD_AMDGPU_USE_NATIVE_PERMLANE64=1` to opt back into the
   native instruction once the backend bug is fixed; the old `QD_AMDGPU_FORCE_PERMLANE64_FALLBACK`
   is now the default and still honored. This is the actual crash fix.

2. The runtime module is compiled by the host x86_64 clang and only retargeted to amdgcn here, so
   `amdgpu_cross_half_shuffle_i32`'s `__asm__ volatile("" : "+v"(byte))` optimization barrier carries
   x86 flag clobbers (`~{dirflag},~{fpsr},~{flags}`) that are meaningless on AMDGPU. The IR verifies
   but the empty-body INLINEASM is invalid on the amdgcn target. Neutralize empty-body barrier asm
   during retarget (forward the tied value, then erase) so no stale host asm reaches codegen. On the
   wave64 targets we ship `ds_bpermute` already addresses the full wave, so the hint is a no-op.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: apply clang-format (v19.1.7) to AMDGPU fn_attrs and launcher sources

CI pre-commit's clang-format hook reformatted these files (long
declarations/lambda signatures collapsed onto single lines per the repo's
clang-format config). Apply the same formatting so the hook passes.

No functional changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(amdgpu): use CreateNeg for branchless i32 sgn instead of CreateSub(0, input)

clang-tidy (modernize-use-nullptr, -warnings-as-errors) flagged
`builder->CreateSub(0, input)` in the i32 sgn path: the literal `0` binds to
the `llvm::Value*` LHS parameter as a null pointer, not an integer zero.
Replace with `builder->CreateNeg(input)`, which emits `0 - input` with a proper
zero constant -- identical intended semantics, and clang-tidy clean.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Robert Dazi <14996868+v01dXYZ@users.noreply.github.com>
Co-authored-by: v01dxyz <v01dxyz@v01d.xyz>
Co-authored-by: Hugh Perkins <hughperkins@gmail.com>
Co-authored-by: Alexis DUBURCQ <alexis.duburcq@gmail.com>
Co-authored-by: hugh <hugh@slurm-login-0.slurm-login.tenant-slurm.svc.cluster.local>
Co-authored-by: alanray-tech <alan.ray@genesis-ai.company>
Co-authored-by: alanray-tech <alanray-tech@users.noreply.github.com>
Co-authored-by: root <root@rtx-209-201.slurm-compute.tenant-slurm.svc.cluster.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Johnny <johnnynuca14@gmail.com>
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