Skip to content

[Lang] whole_kernel_cse: 2.5x compile time speedup on large kernels#577

Merged
duburcqa merged 2 commits into
mainfrom
duburcqa/whole_kernel_cse_speedup
Apr 27, 2026
Merged

[Lang] whole_kernel_cse: 2.5x compile time speedup on large kernels#577
duburcqa merged 2 commits into
mainfrom
duburcqa/whole_kernel_cse_speedup

Conversation

@duburcqa

@duburcqa duburcqa commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

`whole_kernel_cse`: scoped MarkUndone, flat hash bucket, combined walk

Two commits, no behaviour change. Reduces the per-elimination IR-walking cost in WholeKernelCSE from O(N) full-IR sweeps to O(scope) walks (where `scope` is the SSA dominance frontier of the eliminated statement), replaces the per-scope hash-map stack with a flat hash table, swaps a string-allocating type comparison for an RTTI compare, and folds the MarkUndone walk into the replace-usages walk. Affects every kernel that runs through `irpass::full_simplify`; absolute savings are largest on large reverse-mode kernels where autodiff inflates the IR.

TL;DR

`irpass::whole_kernel_cse` runs three to four times per kernel inside `irpass::full_simplify`, with an outer fixpoint loop until no more eliminations are possible. The pass walks the kernel IR, hashes each statement by operand pointers, and replaces a statement with a previously-seen equivalent. Profiling shows it is the dominant compile-time cost on large autodiff kernels:

  • `MarkUndone::run` walked `get_ir_root()` per elimination: O(M * V) total. SSA dominance bounds the user set to the eliminated stmt's parent block subtree plus ancestor top-level stmts (the same scope `replace_all_usages_with(nullptr, ...)` already covers).
  • The visibility table was a stack of per-scope `unordered_map<size_t, unordered_set<Stmt*>>` requiring an O(D) map lookup per stmt visit, where D is nesting depth.
  • Each elimination ran two scope sweeps - one for MarkUndone, one for `replace_all_usages_with` - both checking `has_operand`.
  • `Stmt::type()` (used for the type-mismatch fast path in `common_statement_eliminable`) constructs a `StatementTypeNameVisitor`, dispatches `accept()`, and allocates a `std::string` per call.

Per-kernel measurements

Genesis Ant-scale reverse-mode test (`tests/test_grad.py::test_differentiable_rigid[gpu]`, macOS Metal, fresh compile cache via `QD_OFFLINE_CACHE=0`).

`kernel_forward_dynamics_without_qacc_c305_0` (V=9535, E=14331, 14 cyclic SCCs, 4241 AD-stacks):

Stage `forward_dynamics` compile
Baseline 91 s
Scoped MarkUndone + flat bucket + RTTI 48 s
+ Combined `ReplaceAndMarkUndone` walker 36 s

Smaller kernels in the same test (forward velocity, update_cartesian_space, step_2, compute_qacc) see proportional improvements that are absolute-time invisible because their original CSE cost was already in the few-millisecond range.

Why

Sampling `pytest` during `kernel_forward_dynamics_without_qacc` compilation on macOS Metal showed 100% CPU in the `WholeKernelCSE::visit(Stmt*) -> MarkUndone::run -> BasicStmtVisitor::visit(Block*) -> IfStmt -> ...` recursion chain. The compile cost on this kernel scales with two compounding factors: many CSE candidates per pass, each triggering a full-IR sweep, and a deeply-nested `IfStmt` structure generated by the autodiff transform that inflates the visibility-stack lookup cost.

This pass runs on every kernel through `full_simplify`, so the optimisations apply equally to forward (`AutodiffMode::NONE` / `FORWARD`) and reverse compilations; reverse kernels were where the wallclock cost made the bottleneck visible because their IR is roughly two to three times the size of the forward equivalent.

Mechanism

1. Scope `MarkUndone` to the SSA dominance frontier

`MarkUndone::run` previously walked `modified_operand->get_ir_root()`. Replaced with the same scope `replace_all_usages_with(nullptr, ...)` already uses: walk `modified_operand->parent` (its containing block's full subtree) and then iterate ancestor blocks' top-level statements. Per SSA dominance every user of `modified_operand` lives in this scope, so we cannot miss a stale `visited_` entry. Cost per elimination drops from O(V) to O(scope size); on deeply-nested kernels the eliminated stmt usually lives well below the offloaded body, so the relevant subtree is much smaller than the whole IR.

2. Flat hash bucket plus per-scope insertion log

The visibility table was a `vector<unordered_map<size_t, unordered_set<Stmt*>>>` keyed by scope (one entry per active block). On each `visit(Stmt*)` we walked every scope's map. Replaced with:

  • one global `unordered_map<size_t, vector<Stmt*>>` keyed by `operand_hash`;
  • a `vector<vector<pair<size_t, Stmt*>>>` recording what was inserted at each scope depth.

`visit(Block*)` pushes a fresh insertion log entry, recurses, then on scope exit replays the entry to remove the corresponding stmts from the global table. Sibling subtrees see only ancestor inserts, identical to the per-scope-map semantics. Per-stmt visibility lookup is now O(1) hash plus an O(bucket) bucket walk instead of O(D) hash maps.

3. Combine MarkUndone and replace-usages into `ReplaceAndMarkUndone`

The MarkUndone walk and the `replace_all_usages_with` walk cover the same scope (parent block subtree plus ancestor top-level) and both check `has_operand(modified_operand)` per visited stmt. Folded into a single `ReplaceAndMarkUndone` pass that does both side-effects per visit. Halves the IR-walking cost per elimination.

4. RTTI type compare instead of `Stmt::type()`

`Stmt::type()` constructs a `StatementTypeNameVisitor`, dispatches `accept()` (virtual), and returns a freshly-allocated `std::string`. Called inside `common_statement_eliminable` for every (this_stmt, prev_stmt) pair that shares a hash bucket - which on large kernels was thousands of comparisons per pass.

Replaced with `typeid(*this_stmt) != typeid(*prev_stmt)`. Same correctness contract: the unchecked `prev_stmt->as<...>()` casts immediately below the early-return need this guard, otherwise a hash-bucket collision would reinterpret memory in `definitely_same_address`. After this PR the typeid compare is almost always true at runtime because `operand_hash` now also routes by `typeid(*stmt)` (see below); the guard is kept as the load-bearing safety net.

5. Bonus: `typeid(*stmt)` in `operand_hash`

`operand_hash` previously used `std::hashstd::type_index{}(std::type_index(typeid(stmt)))`. `typeid(stmt)` operates on the `Stmt*` static type for every input, collapsing every concrete statement class into the same hash component - bucket separation came only from operand pointer hashes. Switched to `typeid(*stmt)`, so different statement classes now land in different buckets when their operand sets happen to alias.

Side-effect audit

Concern Where checked Verdict
Visibility-table scope semantics `visit(Block*)` pushes `scope_inserts_`; on exit, replay drops only this-scope entries from `visible_stmts_` Sibling subtrees see only ancestor inserts; identical to the previous per-scope `unordered_map` stack
MarkUndone correctness on dominated users New scope = `modified_operand->parent` subtree plus ancestor top-level stmts SSA dominance guarantees every user of `modified_operand` lives in this scope; no stale `visited_` entry can survive
MarkUndone for stmts with `parent == nullptr` Falls back to `get_ir_root()` walk Defensive only; should not happen for IR built through normal compile paths
`ReplaceAndMarkUndone` matches `replace_all_usages_with` semantics Inherits `BasicStmtVisitor` like the existing `StatementUsageReplace`; same scope traversal, same `has_operand` check, same call site `replace_operand_with` Equivalent traversal; both side-effects fire on the same set of stmts
Type-mismatch guard after `operand_hash` typeid mix `typeid(*this_stmt) != typeid(*prev_stmt)` early-return retained in `common_statement_eliminable` Almost always true at runtime now (hash bucket already keyed by typeid) but load-bearing - the `prev_stmt->as<...>()` casts immediately below are unchecked static-cast wrappers; a hash collision without this guard would reinterpret memory
`typeid(*stmt)` on polymorphic Stmt Stmt has virtual member functions (`quadrants/ir/ir.h`) RTTI returns the dynamic type as required
Public API of `irpass::whole_kernel_cse` Unchanged signature; same one-shot driver `WholeKernelCSE::run(IRNode*)`; same outer fixpoint loop Callers unmodified

…+ ancestors instead of get_ir_root, replace per-scope unordered_map<size_t, unordered_set<Stmt*>> stack with a single flat unordered_map<size_t, vector<Stmt*>> + per-scope insertion log, and use RTTI (typeid(*stmt)) for the type-mismatch fast path in common_statement_eliminable instead of Stmt::type() (which constructs a visitor and allocates a std::string per call)
… and the replace_all_usages_with walk into a single ReplaceAndMarkUndone pass over the same scope (old_stmt's parent block subtree + ancestor top-level stmts), halving the IR-walking cost per CSE elimination on large autodiff kernels

@claude claude 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.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, reopen this pull request to trigger a review.

@duburcqa duburcqa changed the title [Lang] whole_kernel_cse: scope MarkUndone to SSA dominance frontier and combine with replace-usages walk [Lang] whole_kernel_cse: 2.5x compile time speedup on large autodiff kernels Apr 27, 2026
@duburcqa duburcqa changed the title [Lang] whole_kernel_cse: 2.5x compile time speedup on large autodiff kernels [Lang] whole_kernel_cse: 2.5x compile time speedup on large kernels Apr 27, 2026
@hughperkins

Copy link
Copy Markdown
Collaborator

@claude review

1 similar comment
@hughperkins

Copy link
Copy Markdown
Collaborator

@claude review

@claude claude 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.

Verified the correctness arguments hold up (SSA-dominance scope matches StatementUsageReplace, the BasicStmtVisitor vs IRVisitor traversal divergence is benign because StructForStmt/MeshForStmt/OffloadedStmt have no Stmt* operands, and the flat-bucket scope cleanup is sound), but this bundles four subtle optimizations to a critical compiler pass — worth a human compiler-savvy reviewer.

Extended reasoning...

Overview

Single-file change to quadrants/transforms/whole_kernel_cse.cpp bundling four correctness-sensitive performance optimizations to the WholeKernelCSE pass: (1) scoping MarkUndone to the SSA dominance frontier instead of get_ir_root(), (2) replacing the per-scope vector<unordered_map<size_t, unordered_set<Stmt*>>> with a flat unordered_map<size_t, vector<Stmt*>> plus a per-scope insertion log, (3) fusing MarkUndone and replace_all_usages_with into a single ReplaceAndMarkUndone walker, and (4) swapping Stmt::type() string compares for typeid(*stmt) (and fixing operand_hash to use typeid(*stmt) instead of typeid(stmt), which was previously always the Stmt* static type). Public API of irpass::whole_kernel_cse is unchanged.

Security risks

None. This is a compiler IR transformation pass; no auth, crypto, network, or data-exposure surfaces. The risk model is correctness of generated kernel code, not security.

Level of scrutiny

High. whole_kernel_cse runs inside full_simplify for every kernel including reverse-mode autodiff kernels, and a miscompile here produces silently-wrong numerics. The new SSA-dominance scoping and the flat-bucket scope-cleanup logic are correctness-load-bearing — a stale visited_ entry or a leaked visibility entry across sibling scopes would be observable as either missed CSE or, worse, an incorrect cross-scope CSE replacement. I verified the scope claim matches StatementUsageReplace::run(nullptr, ...) (parent block subtree + ancestor top-level statements), the bucket-cleanup correctly preserves outer-scope entries via per-stmt std::find, and the BasicStmtVisitor-vs-IRVisitor traversal divergence is benign because the affected container stmts (StructForStmt, MeshForStmt, OffloadedStmt) have no Stmt* operands in their QD_STMT_DEF_FIELDS so has_operand is always false for them.

Other factors

The PR description includes a detailed side-effect audit table that matches what I see in the code, the per-kernel measurements give a concrete win (91s → 36s on forward_dynamics), and the bug-hunting pass did not flag anything. But the "no behavior change" claim has subtle caveats (the operand_hash change will re-bucket statements differently across runs, the traversal change is a different visitor base class, the visibility table is logically equivalent but a different data structure), and the correctness argument leans on SSA invariants that are easy to state and harder to verify against every IR-producer in the pipeline. A reviewer with compiler-pass context should double-check the SSA-dominance assumption holds for IR shapes produced by the autodiff transform — that's where the wallclock savings are largest and also where the IR is least conventional.

@duburcqa

Copy link
Copy Markdown
Contributor Author

Review by local Claude Opus 4.7:

I've completed a careful review of PR #577. Here's my assessment.

Verdict

Looks correct and ships safely. The correctness rests on existing invariants in the codebase, the perf wins are real, and CI is green across all platforms. A few minor nits worth addressing.

What I verified

1. Scope narrowing of MarkUndone is sound (commit c7190dc02)
The new ReplaceAndMarkUndone::run walks the exact same scope irpass::replace_all_usages_with(nullptr, ...) was already using - confirmed by reading quadrants/transforms/statement_usage_replace.cpp:63-83:
- old_stmt->parent->accept(&walker) (full subtree of containing block)
- For ancestors: top-level statements only via parent_block() chain - no recursion into nested blocks of ancestor siblings

This is identical to the base-branch contract for replace_all_usages_with. By SSA dominance any user of old_stmt lives in this set, so the previous get_ir_root() walk was strictly redundant work. If base was correct, this is correct - and the existing
replace_all_usages_with already trusts the same scope.

The parent == nullptr defensive fallback to get_ir_root() matches the only edge case replace_all_usages_with doesn't handle (it would QD_ASSERT instead).

2. typeid(*stmt) is a genuine bug fix
typeid(stmt) operates on the pointer expression's static type (const Stmt *) and resolves at compile time to the same type_info for every input - the previous operand_hash had zero type discrimination in its hash_type component. typeid(*stmt) dereferences and uses RTTI
on the dynamic type. Stmt has virtual methods (accept, replace_operand_with, etc.) so RTTI works as expected. Main quadrants codebase doesn't compile with -fno-rtti (only external/SPIRV-Tools does).

3. Flat hash bucket + insertion log is semantically equivalent
- Within a single node->accept(&eliminator) traversal, visit(Block*) pushes a frame at entry and replays it at exit, so visible_stmts_ is back to empty at the end. Outer fixpoint iterations see a clean state.
- A stmt is registered at most once per visit(Stmt*) call (mutually exclusive branches), so no double-insert into a single scope's frame.
- Iteration order changes from unordered_set (undefined) to vector (insertion order). More deterministic; doesn't affect correctness because any matched prev_stmt is a valid replacement.

4. Combined ReplaceAndMarkUndone (commit c6f6431e2)
Both side-effects (visited_->erase + replace_operand_with) fire on exactly the stmts where has_operand(old_stmt_) is true. Original code did this in two separate scope traversals; combining them halves the walk cost without changing semantics.

5. Style + CI
- ASCII-only verified (grep -P "[^\x00-\x7F]" clean).
- Single-dash usage throughout, no em-dashes.
- All Build/Test jobs pass on Linux/Mac/Windows + AMD GPU + CUDA.

Nits / suggestions (non-blocking)

N1. Comment on common_statement_eliminable is slightly misleading.
whole_kernel_cse.cpp:120 claims as<...> are "unchecked static-cast convenience wrappers". They're actually dynamic_cast + QD_ASSERT(is<T>()) (ir.h:246-249). The hazard is still real - on type mismatch QD_ASSERT fires QD_ERROR which expands to __builtin_unreachable(), so
 it's UB - but "unchecked static-cast" describes the wrong mechanism. Suggest rewording to "the as<...> casts assert via QD_UNREACHABLE on type mismatch (UB in release)" or similar.

N2. Bucket erase on scope exit is O(B²).
whole_kernel_cse.cpp:196 does std::find(bucket.begin(), bucket.end(), stmt) for each entry in scope_inserts_.back(). For a large bucket where many inserts happen at the same depth, this is quadratic. In practice buckets are tiny because the new typeid(*stmt) mix gives
good separation, so this is unlikely to bite. But since insertion order is preserved and all inserts at this depth land at the end of the bucket, the cleanup could pop from the back in reverse order in O(1) per entry. Worth considering as a micro-follow-up.

N3. register_visible isn't a hot enough abstraction to be a method, but harmless.

What I'd flag for follow-up

- Adding a deterministic CSE-output fingerprint test would help future refactors of this pass; not blocking here.
- The if_stmt branch-merging path at whole_kernel_cse.cpp:222-241 re-parents stmts but doesn't MarkUndone for the moved stmt's operands' users. That's not introduced by this PR, but worth noting if anyone touches this file again.

LGTM as-is; the comment fix at N1 is the only thing I'd suggest before merge.

@github-actions

Copy link
Copy Markdown

Coverage Report (0eb454816)

Metric Value
Diff coverage (changed lines only) 0%
Overall project coverage 73%

Total: 0 lines, 0 missing, 0% covered

@duburcqa

duburcqa commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author
env batch_size backend gjk_collision constraint_solver compile_time_baseline_s compile_time_new_s compile_time_delta_pct runtime_fps_baseline runtime_fps_new runtime_fps_delta_pct realtime_factor_baseline realtime_factor_new realtime_factor_delta_pct
anymal_random 30000 cuda - - 82.9 61.9 -25.33 9423585 9634040 +2.23 94235.9 96340.4 +2.23
anymal_uniform 30000 cuda - - 89.5 62.2 -30.50 12235395 12941962 +5.77 122353.9 129419.6 +5.77
anymal_uniform_kinematic 0 cpu - - 51.4 34.3 -33.27 1992 2038 +2.31 19.9 20.4 +2.51
anymal_uniform_kinematic 30000 cuda - - 84.3 59.8 -29.06 10378314 10395688 +0.17 103783.1 103956.9 +0.17
anymal_zero 0 cpu - - 49.6 30.2 -39.11 6843 7466 +9.10 68.4 74.7 +9.21
anymal_zero 30000 cuda - - 87.9 61.7 -29.81 19072828 19591793 +2.72 190728.3 195917.9 +2.72
box_pyramid_3 4096 cuda - - 116.6 76.8 -34.13 980650 976615 -0.41 9806.5 9766.1 -0.41
box_pyramid_4 4096 cuda - - 112.9 78.4 -30.56 398200 397174 -0.26 3982.0 3971.7 -0.26
box_pyramid_5 4096 cuda - - 108.5 77.7 -28.39 141292 141645 +0.25 1412.9 1416.5 +0.25
box_pyramid_6 4096 cuda False - 111.5 76.1 -31.75 59399 59826 +0.72 594.0 598.3 +0.72
box_pyramid_6 4096 cuda True - 81.2 60.6 -25.37 61883 61462 -0.68 618.8 614.6 -0.68
dex_hand 4096 cuda - - 123.8 89.3 -27.87 17296 17225 -0.41 1081.0 1076.6 -0.41
duck_in_box_easy 30000 cuda False - 83.4 57.1 -31.53 26368796 26596389 +0.86 263688.0 265963.9 +0.86
duck_in_box_easy 30000 cuda True - 54.7 40.6 -25.78 9824790 9803261 -0.22 98247.9 98032.6 -0.22
duck_in_box_hard 0 cpu - - 52.6 33.1 -37.07 4982 5201 +4.40 49.8 52.0 +4.42
duck_in_box_hard 30000 cuda False - 82.8 57.2 -30.92 10040367 10278637 +2.37 100403.7 102786.4 +2.37
duck_in_box_hard 30000 cuda True - 53.5 40.8 -23.74 3557329 3531797 -0.72 35573.3 35318.0 -0.72
franka 30000 cuda - - 78.9 54.5 -30.93 21839164 21930939 +0.42 218391.6 219309.4 +0.42
franka_accessors 0 cpu - - 47.2 30.6 -35.17 1187 1221 +2.86 11.9 12.2 +2.52
franka_accessors 30000 cuda - - 84.9 58.8 -30.74 15371212 15694558 +2.10 153712.1 156945.6 +2.10
franka_free 30000 cuda - - 78.5 54.2 -30.96 32772988 32743296 -0.09 327729.9 327433.0 -0.09
franka_random 0 cpu - - 46.3 30.5 -34.13 5851 6489 +10.90 58.5 64.9 +10.94
franka_random 30000 cuda - CG 78.5 53.7 -31.59 16695511 16927661 +1.39 166955.1 169276.6 +1.39
franka_random 30000 cuda - Newton 76.8 52.5 -31.64 16721019 16848373 +0.76 167210.2 168483.7 +0.76
franka_random 30000 cuda False - 76.9 54.6 -29.00 16857886 16407363 -2.67 168578.9 164073.6 -2.67
franka_random 30000 cuda True - 50.5 36.8 -27.13 11378021 11497596 +1.05 113780.2 114976.0 +1.05
g1_fall 4096 cuda - Newton 97.7 77.8 -20.37 911275 910627 -0.07 4556.4 4553.1 -0.07
go2 4096 cuda False CG 80.8 55.7 -31.06 3610886 3610521 -0.01 36108.9 36105.2 -0.01
go2 4096 cuda False Newton 93.0 75.7 -18.60 4460541 4472146 +0.26 44605.4 44721.5 +0.26
go2 4096 cuda True - 83.4 56.2 -32.61 3240315 3253632 +0.41 32403.2 32536.3 +0.41
shadow_hand_cubes 0 cpu - - 48.8 32.6 -33.20 40 41 +2.50 1.3 1.4 +7.69
shadow_hand_cubes_sparse 0 cpu - - 47.7 31.4 -34.17 65 65 +0.00 2.2 2.2 +0.00

speed_comparison.csv

Comparing genesis/main + Quadrants 0.7.0 (baseline) vs Genesis-Embodied-AI/genesis-world#2743 + this PR

@hughperkins

Copy link
Copy Markdown
Collaborator

Fantastic! 🔥 🔥 🔥

ok to merge.

@duburcqa
duburcqa merged commit d5ab903 into main Apr 27, 2026
51 checks passed
@duburcqa
duburcqa deleted the duburcqa/whole_kernel_cse_speedup branch April 27, 2026 18:51
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants