Skip to content

moe: add planned EXL3 Trellis path for fused Blackwell MoE#49

Open
brandonmmusic-max wants to merge 9 commits into
local-inference-lab:masterfrom
brandonmmusic-max:exl3-trellis-fused
Open

moe: add planned EXL3 Trellis path for fused Blackwell MoE#49
brandonmmusic-max wants to merge 9 commits into
local-inference-lab:masterfrom
brandonmmusic-max:exl3-trellis-fused

Conversation

@brandonmmusic-max

@brandonmmusic-max brandonmmusic-max commented Jul 20, 2026

Copy link
Copy Markdown

Summary

Adds the EXL3 Trellis compute path and a stable planned MoE API to Sparkinfer.
It consumes native EXL3 MCG-codebook tensors without repacking or offline
requantization and supports fused routed-expert execution on Blackwell.

This PR includes:

  • an EXL3 Trellis W4A16 arm for 3/4/5/6 bpw tensors
  • dense and routed-MoE prepare, plan, and execute interfaces
  • a public sparkinfer.moe.trellis_moe API used by vLLM
  • exact per-projection rotation handling and grouped routed execution
  • compile-cache keys that include device compute capability
  • focused API, route-pack, plan, cache, and correctness tests

The production consumer is
local-inference-lab/vllm#139.

Rebase and release pins

  • base: master at c39b8062ba450c030e669d898a026d10980c9470
  • PR head: d4438d490691f79022fdfc8149e1c5f161d15445
  • vLLM consumer head: 26c4bfdd3ff2be0433e6fe07e0c3be535f5bb318
  • CUDA 13.2, Torch 2.12, SM120a

The exact validated image is:

verdictai/glm52-exl3-sparkinfer:v26-gg-v20final-scopefix-archkey-vllm5517197-sibe0edca-cu132-sm120a@sha256:2bb9e804a283d1da3b7e3425ff87375121285141d0d0a40d3dc09d41bf881a10

Finalized Gilded Gnosis v20 common base (2026-07-25)

This branch is rebased onto its own base branch for review and merge. It has also
been built and validated as an EXL3 source layer on the finalized v20 common
base. Per the v20 release note the base packages no EXL3/Trellis loader or
kernels, so EXL3 is enabled by rebasing the EXL3 layer onto the pinned stack --
not by pointing the voipmonitor/vllm tag at an EXL3 checkpoint.

  • base image: voipmonitor/vllm:gilded-gnosis-v20-vllm5517197-sibe0edca-fi801d57a-cu132-20260725
  • EXL3 vLLM layer rebased onto vLLM 551719766029e78824a30d97ae6ac63917405b5f
  • EXL3 Sparkinfer layer rebased onto SparkInfer be0edcaae6f5d284bb29a82325aba7a0ead6960f
  • Of the 12 runtime overlay files, only models/deepseek_v2.py and
    v1/attention/backends/mla/indexer.py differ between the previous and the
    finalized v20 tree. Both were re-derived from the v20-final versions with the
    EXL3 edits replayed on top, so the new DCP/indexer/prefill work is preserved
    rather than overwritten by the overlay.

Validated image:

verdictai/glm52-exl3-sparkinfer:v26-gg-v20final-scopefix-archkey-vllm5517197-sibe0edca-cu132-sm120a@sha256:2bb9e804a283d1da3b7e3425ff87375121285141d0d0a40d3dc09d41bf881a10

Runtime validation, 4x RTX PRO 6000 Blackwell (SM120a), TP4/DCP4, MTP-3, NVFP4 DS-MLA KV:

  • engine reports v0.11.2.dev280+gilded.gnosis.v20.vllm5517197.sibe0edca.fi801d57a.cu132.20260725
  • vLLM is using nccl==2.30.4
  • EXL3 planned: Trellis m=1..32 block_m=8, prefill trellis block_m=64 arena=1054.2MiB capacity=3072 chunk=128 topk=8
  • v20 DCP prefill auto-policy resolved for TP4/DCP4 (DCP_*=auto): query split 1,
    full CKV gather 1, top-k owner merge 1, indexer shards 0, CKV prefetch depth 1,
    and DCP_PREFILL_WORKSPACE=1 (VLLM_DCP_PROJECT_BEFORE_MERGE=1 +
    VLLM_B12X_MLA_DCP_GATHER_IN_WORKSPACE=1)
  • Preallocated 30.8 MiB for 2 persistent CKV execution lane(s) and
    Using native CKV layer prefetch with depth=1 and 2 workspace slots
  • GPU KV cache 998,656 tokens (1.90x at 524,288 per request). This is ~1 GiB lower
    than the pre-fix image because the draft model now gets its own rank-sliced
    runtime arena instead of sharing the target's -- the intended cost of the
    target/draft scratch isolation.
  • Estonia c2 x5: 5/5 pass, 0 fail, correct rate 1.00
  • LAVD c5 x5: 3 EXACT / 2 NEAR / 0 FAIL, correct rate 1.00

Earlier images in this series measured LAVD at 2E/3N/0F (pre-fix) and, on one
5-run sample of the first scoped build, 2E/2N/1F; a second sample of that build
returned 2E/3N/0F. The single failure there was a wrong ledger total at 7,114
completion tokens against a 24,576 cap -- an answer-quality miss rather than
truncation, and within this profile's run-to-run spread. This build shows no
failures on either suite.

Note: the base's Loading safetensors using InstantTensor loader assertion does
not appear on this path -- the EXL3 checkpoint is loaded by the EXL3 rank-sliced
loader, not the base InstantTensor path. All other boot assertions hold.

Validation

The kernel ladder was validated on RTX PRO 6000 Blackwell (SM120a), including
bitrate-specific reconstruction, batch-shape stability, and CUDA graph replay.

Focused exact-image results:

Sparkinfer EXL3/API/Trellis suite: 10 passed
production-layer oracle, E=256 H=6144 I=512 top-k=8 m=8:
  relative error 0.000872
  cosine similarity 0.999999642

End-to-end validation used the 332.19 GB GLM-5.2 rank-sliced EXL3 checkpoint on
TP4 + DCP4 A2A with MTP1 greedy, NVFP4 DeepSeek-MLA KV cache, and async
scheduling disabled. All 81 shards loaded, CUDA graphs captured through C8,
LAVD passed 20/20 across two full runs, and Estonia passed 5/5.

Exact-image performance:

Test Result
8K / 64K / 128K cold prefill 1,502 / 1,249 / 1,182 tok/s
C1 / C2 / C3 / C4 sustained decode 48.9 / 112.9 / 154.0 / 188.2 aggregate tok/s
C5 / C6 / C7 / C8 sustained decode 218.2 / 239.4 / 253.9 / 266.8 aggregate tok/s

Notes and limits

  • The optimized path requires MCG-codebook EXL3 weights with compatible K/N
    alignment and is performance-validated on SM120a.
  • Unsupported codebooks and shapes are rejected by prepare-time validation;
    the vLLM consumer owns its parity fallback.
  • Small routed batches use the planned fused path. Larger prefill batches are
    chunked by the consumer according to its configured capacity.
  • SM100 portability is not claimed as validated performance support.
  • The published serving preset keeps async scheduling disabled because the
    remaining release constraint is in vLLM speculative/CKV lifetime management,
    not this Trellis compute API.

AI assistance disclosure

Implementation, debugging, and documentation were developed with Codex and
Claude Code assistance. The author selected the design, reviewed the changes,
and ran the reported hardware validation.

Summary by CodeRabbit

  • New Features
    • Added a Trellis-based MoE operator with weight preparation, planning, binding, and execution, plus support detection.
    • Introduced Trellis256 dense execution and extended trellis dequant intrinsics for multiple bitrates (FP16 and BF16).
    • Expanded W4A16 MoE utilities with full-rotation mode, configurable routing block sizing, and new dense trellis weight preparation.
  • Bug Fixes
    • Compile caches now key on GPU device architecture to avoid incorrect cross-device reuse.
  • Tests
    • Added end-to-end Trellis MoE tests (including CUDA graph capture), compile-cache device-arch coverage, and a high-page-id attention paging regression test.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds Trellis3.0 decoding and native Trellis256 weight preparation, extends W4A16 GEMM and full-rotation MoE execution, exposes a lazy-loaded operator API, and updates compile-cache payloads and manifest validation for device-aware formats.

Changes

Trellis MoE and cache integration

Layer / File(s) Summary
Device-aware compile-cache formats
sparkinfer/_lib/compiler.py, validation/cutlass_migration/..., tests/_lib/test_compile_cache.py
Cache payloads include device architecture data, explicit-spec formats advance to v6, non-explicit formats to v3, and manifest parsing and integrity checks use the revised indices.
Trellis decoding and weight preparation
sparkinfer/_lib/intrinsics.py, sparkinfer/moe/_shared/kernels/w4a16/prepare.py, sparkinfer/moe/_shared/kernels/w4a16/__init__.py
Adds bitrate-aware Trellis FP16/BF16 decoding, native Trellis256 MoE and dense weight preparation, metadata validation, and public exports.
Trellis256 GEMM and full-rotation kernels
sparkinfer/moe/_shared/kernels/w4a16/kernel.py
Adds Trellis layouts, dequantization, alternate-A paths, dense routing, rotation-aware activation and top-k accumulation, expanded compilation and launch contracts, and dense Trellis256 execution.
Public planning and runtime flow
sparkinfer/moe/trellis_moe/*, sparkinfer/moe/__init__.py, sparkinfer/__init__.py
Registers the operator and adds capability, preparation, planning, scratch binding, execution, support detection, and cache-clearing APIs.
Buffers, routing, dependency wiring, and tests
sparkinfer/moe/_shared/kernels/w4a16/host.py, sparkinfer/moe/_shared/kernels/w4a16/route_pack.py, tests/moe/test_trellis_moe.py, tests/attention/test_fused_indexer.py, pyproject.toml
Adds configurable block sizing, full-rotation buffers, reusable expert-count storage, the Triton runtime dependency, validation tests, reference reconstruction, CUDA graph checks, and a high-page-ID indexer regression test.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TrellisMoEAPI
  participant WeightPreparation
  participant W4A16Kernels
  participant TopKSum
  Caller->>TrellisMoEAPI: prepare_weights, plan, bind
  TrellisMoEAPI->>WeightPreparation: validate and wrap native Trellis weights
  WeightPreparation-->>TrellisMoEAPI: TrellisMoEWeights
  TrellisMoEAPI->>W4A16Kernels: run full-rotation fused MoE
  W4A16Kernels->>TopKSum: activation output and route metadata
  TopKSum-->>TrellisMoEAPI: accumulated output
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a planned EXL3 Trellis MoE path for Blackwell.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3ff9087-de31-44d4-ba04-a0a09dbdaff1

📥 Commits

Reviewing files that changed from the base of the PR and between 957b9fd and e611971.

📒 Files selected for processing (10)
  • b12x/cute/compiler.py
  • b12x/cute/fp4.py
  • b12x/moe/fused/w4a16/__init__.py
  • b12x/moe/fused/w4a16/host.py
  • b12x/moe/fused/w4a16/kernel.py
  • b12x/moe/fused/w4a16/prepare.py
  • tests/test_cutlass_runtime_patches.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • validation/cutlass_migration/evidence/kernel_resources.py

Comment thread validation/cutlass_migration/acceptance/corpus/ptx_capture.py
@brandonmmusic-max brandonmmusic-max changed the title w4a16: EXL3 trellis (trellis3_t256) fused arm — dense + MoE, 3/4/5/6 bpw, byte-identical across m moe: add planned EXL3 Trellis path for fused Blackwell MoE Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
sparkinfer/moe/trellis_moe/api.py (1)

41-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

__all__ is not isort-sorted (RUF022). Sort to keep lint green.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sparkinfer/moe/trellis_moe/api.py` around lines 41 - 52, Sort the exported
names in the module-level __all__ list alphabetically to satisfy RUF022,
preserving every existing export and its spelling.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sparkinfer/moe/_shared/kernels/w4a16/kernel.py`:
- Around line 3481-3525: Change the trellis256 B-source offset calculations in
both branches of the shown loading loop to use Int64 for all offset arithmetic,
including expert, tile, projection, and local terms. Ensure b_src_i32 is widened
consistently before forming the final source address, matching the sibling
weight-loading path and preventing valid large-shape offsets from overflowing
Int32.

In `@sparkinfer/moe/trellis_moe/__init__.py`:
- Line 69: Add triton to the project dependencies in pyproject.toml, matching
the existing dependency declaration conventions. Keep the requires=("triton",)
metadata in the trellis_moe module unchanged so support detection and runtime
requirements remain aligned.

---

Nitpick comments:
In `@sparkinfer/moe/trellis_moe/api.py`:
- Around line 41-52: Sort the exported names in the module-level __all__ list
alphabetically to satisfy RUF022, preserving every existing export and its
spelling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 619928cf-145f-4ffe-a35a-4ddb9590c87e

📥 Commits

Reviewing files that changed from the base of the PR and between d4f82a6 and 1937274.

📒 Files selected for processing (17)
  • sparkinfer/__init__.py
  • sparkinfer/_lib/compiler.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/host.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py
  • sparkinfer/moe/_shared/kernels/w4a16/prepare.py
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • sparkinfer/moe/trellis_moe/__init__.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/moe/trellis_moe/api.py
  • tests/_lib/test_compile_cache.py
  • tests/moe/test_trellis_moe.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • validation/cutlass_migration/evidence/kernel_resources.py

Comment thread sparkinfer/moe/_shared/kernels/w4a16/kernel.py
Comment thread sparkinfer/moe/trellis_moe/__init__.py
brandonmmusic-max added a commit to brandonmmusic-max/b12x that referenced this pull request Jul 22, 2026
- w4a16 kernel: compute the trellis256 B-source offset in Int64 in both
  the proj and non-proj branches. The expert-stride term overflows Int32
  for large-but-valid MoE shapes (e.g. E=256, H=8192, bits=6 reaches
  ~3.21e9 > 2^31-1), which would fetch wrong or out-of-bounds B weights.
  Matches the sibling modelopt-native staging path that already builds
  this offset with Int64(...). Value-identical for in-range offsets, so
  existing shapes/tests are unaffected.
- pyproject: declare the `triton` dependency. trellis_moe META.requires
  it and is_supported() gates on it, so a clean install lacking triton
  would silently skip the op.
- trellis_moe.api: sort __all__ (RUF022).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brandonmmusic-max added a commit to brandonmmusic-max/b12x that referenced this pull request Jul 22, 2026
- w4a16 kernel: compute the trellis256 B-source offset in Int64 in both
  the proj and non-proj branches. The expert-stride term overflows Int32
  for large-but-valid MoE shapes (e.g. E=256, H=8192, bits=6 reaches
  ~3.21e9 > 2^31-1), which would fetch wrong or out-of-bounds B weights.
  Matches the sibling modelopt-native staging path that already builds
  this offset with Int64(...). Value-identical for in-range offsets, so
  existing shapes/tests are unaffected.
- pyproject: declare the `triton` dependency. trellis_moe META.requires
  it and is_supported() gates on it, so a clean install lacking triton
  would silently skip the op.
- trellis_moe.api: sort __all__ (RUF022).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sparkinfer/moe/_shared/kernels/w4a16/host.py`:
- Around line 395-412: Change the full-rotation activation buffers
rotation_a_gate and rotation_a_up to allocate torch.bfloat16, and update the
corresponding full-rotation scratch validation and kernel ABI expectations used
by run_w4a16_moe() to require BF16 consistently. Preserve the existing
conditional allocation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f5a8e43-01e2-4b48-aaec-40ef680de1e3

📥 Commits

Reviewing files that changed from the base of the PR and between fd1d7b5 and 879ca0a.

📒 Files selected for processing (18)
  • pyproject.toml
  • sparkinfer/__init__.py
  • sparkinfer/_lib/compiler.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/host.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py
  • sparkinfer/moe/_shared/kernels/w4a16/prepare.py
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • sparkinfer/moe/trellis_moe/__init__.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/moe/trellis_moe/api.py
  • tests/_lib/test_compile_cache.py
  • tests/moe/test_trellis_moe.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • validation/cutlass_migration/evidence/kernel_resources.py
🚧 Files skipped from review as they are similar to previous changes (13)
  • sparkinfer/moe/_shared/kernels/w4a16/init.py
  • sparkinfer/init.py
  • sparkinfer/moe/init.py
  • sparkinfer/moe/trellis_moe/init.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • sparkinfer/moe/trellis_moe/api.py
  • validation/cutlass_migration/evidence/kernel_resources.py
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • tests/moe/test_trellis_moe.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py

Comment thread sparkinfer/moe/_shared/kernels/w4a16/host.py
davidsyoung pushed a commit to davidsyoung/b12x that referenced this pull request Jul 22, 2026
Unblocks building the trellis3_t256 stack from local-inference-lab/
b12x#49 (@brandonmusic) on the public PyPI nvidia-cutlass-dsl 4.6.0
wheel, which local-inference-lab#49 requires (>= 4.6.0) but currently cannot run on.

The DSL wheels disagree about both the fmax ABI and what
cutlass.CUDA_VERSION means: the PyPI 4.6.0 wheel reports its bundled
CTK as DSLCudaVersion(12, 9) while exposing the unified
nvvm.fmax(a, b, *, c=None, ...) signature, so the existing
"major == 12 and minor == 9" branch calls the removed
nvvm.fmax(T.f32(), a, b) form and every attention JIT fails at engine
init with "fmax() takes 2 positional arguments but 3 ... were given".
The CTK-13.3 DSL build used by the published runtime image sidesteps
the sniff by reporting 13.3; PyPI users hit it every time.

Probe the actual signature once at import (first parameter named "a"
or "b" means the unified form) and pick the call shape from that.
Verified on PyPI 4.5.2 (legacy result-type form preserved), PyPI
4.6.0 (unified; attention JIT compiles clean), and the CTK-13.3 build
(unified; same branch it already takes).
@brandonmmusic-max

Copy link
Copy Markdown
Author

Great job here man!

before it lands, #163 looks like it's stacked on an earlier snapshot oof my pr 139. The three shared files it carries —

  • vllm/v1/structured_output/__init__.py
  • vllm/models/deepseek_v32/nvidia/attention.py
  • vllm/v1/attention/backends/mla/b12x_mla_sparse.py

— are from before the MTP + tool-call fixes I pushed to #139 today. Concretely, #163's should_advance() is still the old signature (no new_token_ids), and it's missing:

  • the reasoning-boundary grammar fix that makes tool calling work under MTP speculative decoding (vLLM #48516),
  • the GLM-5.2 DSA has_indexer/index_k crash guard on MTP draft steps 1+ (vLLM #48528),
  • the multi-row MTP causal-verification fix in the B12X sparse-MLA decode kernel.

Could you rebase onto the current #139 head (2fdc6d3676) — or we land #139 into dev/gilded-gnosis first and you rebase on top — so the prefill work sits on top of the fixes instead of an older base?

davidsyoung pushed a commit to davidsyoung/vllm that referenced this pull request Jul 22, 2026
Builds on the rank-sliced EXL3 (ExLlamaV3 trellis) support added in
local-inference-lab#139 by @brandonmusic, paired with the
trellis3_t256 planned MoE from local-inference-lab/sparkinfer#49. No change
to that work's weight loading, formats, or decode path; this commit
only extends its batch dispatch.

The vllm-project#139 backend serves every batch with m > 32 through the eager
exllamav3_ext parity path: fp16-in/fp32-out staging (~25 GB extra HBM
traffic per 4096-token step at GLM-5.2 geometry), python chunk loops,
and one expert re-stream per 128-row chunk. That covers 100% of
prefill, so prefill throughput is capped at ~2.0-2.3k tok/s while the
same weights decode at full speed through the planned Trellis window.

Build a second trellis_moe plan at max_tokens=max_num_batched_tokens
with block_size_m=64 (env VLLM_EXL3_PREFILL_BLOCK_M, allowed
{8,16,32,48,64}) beside the existing decode plan (32/8), and dispatch
three ways in _apply_rank_sliced:

* m in [min, max]      -> decode plan (unchanged)
* max < m <= capacity  -> prefill plan (sparkinfer bind already accepts
                          any tokens in [1, max_tokens]; zero kernel
                          changes needed)
* m < min              -> parity path, whose persistent staging
                          (xh/out32/token_sorted/weight_sorted) shrinks
                          from capacity rows to one chunk while the
                          prefill plan is live (~110 MiB/GPU returned)

VLLM_EXL3_PREFILL_TRELLIS=0 restores the exact single-plan parity
behavior (one-variable serving A/B lever). The parity branch now raises
during CUDA graph capture instead of silently recording eager ext
calls. The prefill arena (~1.05 GiB/GPU at capacity 3072) allocates
during the profile pass, so vLLM's memory profiler sees it and the KV
pool auto-shrinks instead of risking request-time OOM.

Matched A/B serving brandonmusic/GLM-5.2-EXL3-TR3-3.0bpw on the
published verdictai/glm52-exl3-sparkinfer runtime image (4x RTX PRO
6000 Blackwell, TP4/DCP4/MTP3, identical boot geometry, only the
kill-switch flipped, fresh JIT cache both arms; llm_decode_bench
prefill-only, exact /tokenize, C1, 60 s/context):

  ctx    parity   trellis   delta
  8k      2,287     3,742   +63.6%
  32k     2,218     3,557   +60.4%
  64k     2,078     3,381   +62.7%
  128k    1,920     3,032   +57.9%

Decode C1 unchanged-to-better; 57k-token needle retrieval exact.
brandonmmusic-max pushed a commit to brandonmmusic-max/vllm that referenced this pull request Jul 22, 2026
Builds on the rank-sliced EXL3 (ExLlamaV3 trellis) support added in
local-inference-lab#139 by @brandonmusic, paired with the
trellis3_t256 planned MoE from local-inference-lab/sparkinfer#49. No change
to that work's weight loading, formats, or decode path; this commit
only extends its batch dispatch.

The vllm-project#139 backend serves every batch with m > 32 through the eager
exllamav3_ext parity path: fp16-in/fp32-out staging (~25 GB extra HBM
traffic per 4096-token step at GLM-5.2 geometry), python chunk loops,
and one expert re-stream per 128-row chunk. That covers 100% of
prefill, so prefill throughput is capped at ~2.0-2.3k tok/s while the
same weights decode at full speed through the planned Trellis window.

Build a second trellis_moe plan at max_tokens=max_num_batched_tokens
with block_size_m=64 (env VLLM_EXL3_PREFILL_BLOCK_M, allowed
{8,16,32,48,64}) beside the existing decode plan (32/8), and dispatch
three ways in _apply_rank_sliced:

* m in [min, max]      -> decode plan (unchanged)
* max < m <= capacity  -> prefill plan (sparkinfer bind already accepts
                          any tokens in [1, max_tokens]; zero kernel
                          changes needed)
* m < min              -> parity path, whose persistent staging
                          (xh/out32/token_sorted/weight_sorted) shrinks
                          from capacity rows to one chunk while the
                          prefill plan is live (~110 MiB/GPU returned)

VLLM_EXL3_PREFILL_TRELLIS=0 restores the exact single-plan parity
behavior (one-variable serving A/B lever). The parity branch now raises
during CUDA graph capture instead of silently recording eager ext
calls. The prefill arena (~1.05 GiB/GPU at capacity 3072) allocates
during the profile pass, so vLLM's memory profiler sees it and the KV
pool auto-shrinks instead of risking request-time OOM.

Matched A/B serving brandonmusic/GLM-5.2-EXL3-TR3-3.0bpw on the
published verdictai/glm52-exl3-sparkinfer runtime image (4x RTX PRO
6000 Blackwell, TP4/DCP4/MTP3, identical boot geometry, only the
kill-switch flipped, fresh JIT cache both arms; llm_decode_bench
prefill-only, exact /tokenize, C1, 60 s/context):

  ctx    parity   trellis   delta
  8k      2,287     3,742   +63.6%
  32k     2,218     3,557   +60.4%
  64k     2,078     3,381   +62.7%
  128k    1,920     3,032   +57.9%

Decode C1 unchanged-to-better; 57k-token needle retrieval exact.

(cherry picked from commit d57d775)
brandonmmusic-max added a commit to brandonmmusic-max/b12x that referenced this pull request Jul 22, 2026
- w4a16 kernel: compute the trellis256 B-source offset in Int64 in both
  the proj and non-proj branches. The expert-stride term overflows Int32
  for large-but-valid MoE shapes (e.g. E=256, H=8192, bits=6 reaches
  ~3.21e9 > 2^31-1), which would fetch wrong or out-of-bounds B weights.
  Matches the sibling modelopt-native staging path that already builds
  this offset with Int64(...). Value-identical for in-range offsets, so
  existing shapes/tests are unaffected.
- pyproject: declare the `triton` dependency. trellis_moe META.requires
  it and is_supported() gates on it, so a clean install lacking triton
  would silently skip the op.
- trellis_moe.api: sort __all__ (RUF022).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brandonmmusic-max

Copy link
Copy Markdown
Author

Rebased onto SparkInfer master 1a88b389 (Gilded Gnosis v20 canonical)

Force-pushed a rebase of this PR onto master 1a88b389 (the v20 canonical SparkInfer head). The four EXL3 Trellis commits replay cleanly on top of v20:

  • device compute capability in the compile disk-cache key
  • the trellis3_t256 fused W4A16 arm
  • the planned EXL3 Trellis MoE API
  • CodeRabbit review fixes

The packed-page direct-K high-page-id i64-offset test is retained.

Validation (paired with vLLM #139, on the v20 EXL3 image)

Test v20 Prior base
MTP-3 code generation 94–100% valid — reported ~50% syntax-error symptom eliminated ~50%
LAVD r10/c5 10/10 (0 fail) 8/10 (2 fail)

Tested image: verdictai/glm52-exl3-sparkinfer:v20-gg6722c1d-si1a88b38-cu132-sm120a.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
sparkinfer/moe/_shared/kernels/w4a16/prepare.py (1)

1564-1565: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: use iterable unpacking instead of tuple concatenation.

Ruff flags these two lines (RUF005). Purely stylistic; functionally equivalent.

♻️ Suggested tweak
-    expected_i16_shape = expected_prefix_shape + (16 * trellis_bits,)
-    expected_i32_shape = expected_prefix_shape + (8 * trellis_bits,)
+    expected_i16_shape = (*expected_prefix_shape, 16 * trellis_bits)
+    expected_i32_shape = (*expected_prefix_shape, 8 * trellis_bits)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sparkinfer/moe/_shared/kernels/w4a16/prepare.py` around lines 1564 - 1565,
Update the expected_i16_shape and expected_i32_shape assignments to use iterable
unpacking for appending their final dimension instead of tuple concatenation,
preserving the existing shapes and values.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sparkinfer/_lib/compiler.py`:
- Around line 2047-2065: Update _semantic_compile_manifest_payload to include
cache_payload[4] as device_arch in semantic_payload for both explicit and
non-explicit cache formats, preserving all existing index mappings. Update
_build_compile_manifest to expose the same device_arch value alongside toolchain
so manifest semantic identity remains device-aware and consistent across
builders.

---

Nitpick comments:
In `@sparkinfer/moe/_shared/kernels/w4a16/prepare.py`:
- Around line 1564-1565: Update the expected_i16_shape and expected_i32_shape
assignments to use iterable unpacking for appending their final dimension
instead of tuple concatenation, preserving the existing shapes and values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3224cf74-0a61-4973-a96d-cd82260c5596

📥 Commits

Reviewing files that changed from the base of the PR and between 879ca0a and 5dd6f15.

📒 Files selected for processing (19)
  • pyproject.toml
  • sparkinfer/__init__.py
  • sparkinfer/_lib/compiler.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/host.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py
  • sparkinfer/moe/_shared/kernels/w4a16/prepare.py
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • sparkinfer/moe/trellis_moe/__init__.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/moe/trellis_moe/api.py
  • tests/_lib/test_compile_cache.py
  • tests/attention/test_fused_indexer.py
  • tests/moe/test_trellis_moe.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • validation/cutlass_migration/evidence/kernel_resources.py
🚧 Files skipped from review as they are similar to previous changes (16)
  • sparkinfer/moe/_shared/kernels/w4a16/init.py
  • pyproject.toml
  • sparkinfer/init.py
  • tests/_lib/test_compile_cache.py
  • sparkinfer/moe/trellis_moe/init.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • sparkinfer/moe/trellis_moe/api.py
  • sparkinfer/moe/init.py
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • tests/moe/test_trellis_moe.py
  • validation/cutlass_migration/evidence/kernel_resources.py
  • sparkinfer/moe/_shared/kernels/w4a16/host.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py

Comment thread sparkinfer/_lib/compiler.py
@brandonmmusic-max

Copy link
Copy Markdown
Author

@coderabbitai review

Fixes are in — device_arch is now included in the semantic manifest identity (63bcedf6), so payloads differing only by GPU architecture get distinct semantic keys. Done with changes; please re-verify.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline sparkinfer/_lib/compiler.py --items all --type function --match 'device_arch|semantic_compile_manifest_payload|build_compile_manifest|compile_disk_cache_payload|static_compile_cache_context'
ast-grep outline tests/_lib/test_compile_cache.py --items all --type function --match 'device_arch|disk_cache_key'

printf '\n--- compiler references ---\n'
rg -n -C 5 '_device_arch_key|_static_compile_cache_context|_compile_disk_cache_payload|_semantic_compile_manifest_payload|_build_compile_manifest|semantic' sparkinfer/_lib/compiler.py

printf '\n--- focused tests ---\n'
rg -n -C 8 'test_disk_cache_key_includes_device_arch|test_device_arch_key_retries_after_unavailable|device_arch' tests/_lib/test_compile_cache.py

Length of output: 13287


@brandonmmusic-max Confirmed. device_arch is threaded into the disk-cache payload at index 4, explicitly included in _semantic_compile_manifest_payload(), and therefore participates in the deterministic hash used for manifest["semantic_key"]. Payloads that differ only by GPU architecture now produce distinct semantic identities.

The focused tests also cover payload inclusion and retry behavior when CUDA is initially unavailable. Re-reviewing the PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

lukealonso pushed a commit that referenced this pull request Jul 23, 2026
Unblocks building the trellis3_t256 stack from local-inference-lab/
b12x#49 (@brandonmusic) on the public PyPI nvidia-cutlass-dsl 4.6.0
wheel, which #49 requires (>= 4.6.0) but currently cannot run on.

The DSL wheels disagree about both the fmax ABI and what
cutlass.CUDA_VERSION means: the PyPI 4.6.0 wheel reports its bundled
CTK as DSLCudaVersion(12, 9) while exposing the unified
nvvm.fmax(a, b, *, c=None, ...) signature, so the existing
"major == 12 and minor == 9" branch calls the removed
nvvm.fmax(T.f32(), a, b) form and every attention JIT fails at engine
init with "fmax() takes 2 positional arguments but 3 ... were given".
The CTK-13.3 DSL build used by the published runtime image sidesteps
the sniff by reporting 13.3; PyPI users hit it every time.

Probe the actual signature once at import (first parameter named "a"
or "b" means the unified form) and pick the call shape from that.
Verified on PyPI 4.5.2 (legacy result-type form preserved), PyPI
4.6.0 (unified; attention JIT compiles clean), and the CTK-13.3 build
(unified; same branch it already takes).
@brandonmmusic-max

Copy link
Copy Markdown
Author

Validation — this PR's fused Trellis MoE kernels drive a tr3 MTP draft layer

Confirming the fused Trellis MoE kernels from this PR (si1a88b38) correctly run a rank-sliced tr3 MTP draft layer (GLM-5.2 MTP layer-78), not only the main decoder layers. No sparkinfer change was required — the two loader fixes were in vLLM (local-inference-lab/vllm#139).

One runtime knob: the MTP-N draft issues small-m GEMMs (m=1…N), so the Trellis decode window must include them — set VLLM_EXL3_TRELLIS_MIN_M=1 (default 4eager parity path entered during CUDA graph capture (m=3)).

Image: verdictai/glm52-exl3-sparkinfer:v21-mtp78tr3-gg6722c1d-si1a88b38-cu132-sm120a
Validated TP4/DCP4/MTP-3 on GLM-5.2-EXL3-TR3-3.0bpw + tr3 MTP-78: loads clean, Estonia 10/10, LAVD EXACT 5 / NEAR 5 / FAIL 0, KV 1.13M tokens @ util 0.96 (2.16× @ 524K).

brandonmmusic-max and others added 6 commits July 25, 2026 17:28
Co-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Brandon Music <brandon.m.music@gmail.com>
- w4a16 kernel: compute the trellis256 B-source offset in Int64 in both
  the proj and non-proj branches. The expert-stride term overflows Int32
  for large-but-valid MoE shapes (e.g. E=256, H=8192, bits=6 reaches
  ~3.21e9 > 2^31-1), which would fetch wrong or out-of-bounds B weights.
  Matches the sibling modelopt-native staging path that already builds
  this offset with Int64(...). Value-identical for in-range offsets, so
  existing shapes/tests are unaffected.
- pyproject: declare the `triton` dependency. trellis_moe META.requires
  it and is_supported() gates on it, so a clean install lacking triton
  would silently skip the op.
- trellis_moe.api: sort __all__ (RUF022).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
device_arch (cache_payload index 4) was read into the disk-cache key but
dropped from _semantic_compile_manifest_payload, so two payloads differing
only by GPU architecture hashed to the same semantic identity. Add it to the
common semantic dict (index 4 in both v6_explicit_spec and v3 formats).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
brandonmmusic-max pushed a commit to brandonmmusic-max/vllm that referenced this pull request Jul 25, 2026
Builds on the rank-sliced EXL3 (ExLlamaV3 trellis) support added in
local-inference-lab#139 by @brandonmusic, paired with the
trellis3_t256 planned MoE from local-inference-lab/sparkinfer#49. No change
to that work's weight loading, formats, or decode path; this commit
only extends its batch dispatch.

The vllm-project#139 backend serves every batch with m > 32 through the eager
exllamav3_ext parity path: fp16-in/fp32-out staging (~25 GB extra HBM
traffic per 4096-token step at GLM-5.2 geometry), python chunk loops,
and one expert re-stream per 128-row chunk. That covers 100% of
prefill, so prefill throughput is capped at ~2.0-2.3k tok/s while the
same weights decode at full speed through the planned Trellis window.

Build a second trellis_moe plan at max_tokens=max_num_batched_tokens
with block_size_m=64 (env VLLM_EXL3_PREFILL_BLOCK_M, allowed
{8,16,32,48,64}) beside the existing decode plan (32/8), and dispatch
three ways in _apply_rank_sliced:

* m in [min, max]      -> decode plan (unchanged)
* max < m <= capacity  -> prefill plan (sparkinfer bind already accepts
                          any tokens in [1, max_tokens]; zero kernel
                          changes needed)
* m < min              -> parity path, whose persistent staging
                          (xh/out32/token_sorted/weight_sorted) shrinks
                          from capacity rows to one chunk while the
                          prefill plan is live (~110 MiB/GPU returned)

VLLM_EXL3_PREFILL_TRELLIS=0 restores the exact single-plan parity
behavior (one-variable serving A/B lever). The parity branch now raises
during CUDA graph capture instead of silently recording eager ext
calls. The prefill arena (~1.05 GiB/GPU at capacity 3072) allocates
during the profile pass, so vLLM's memory profiler sees it and the KV
pool auto-shrinks instead of risking request-time OOM.

Matched A/B serving brandonmusic/GLM-5.2-EXL3-TR3-3.0bpw on the
published verdictai/glm52-exl3-sparkinfer runtime image (4x RTX PRO
6000 Blackwell, TP4/DCP4/MTP3, identical boot geometry, only the
kill-switch flipped, fresh JIT cache both arms; llm_decode_bench
prefill-only, exact /tokenize, C1, 60 s/context):

  ctx    parity   trellis   delta
  8k      2,287     3,742   +63.6%
  32k     2,218     3,557   +60.4%
  64k     2,078     3,381   +62.7%
  128k    1,920     3,032   +57.9%

Decode C1 unchanged-to-better; 57k-token needle retrieval exact.

(cherry picked from commit d57d775)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
sparkinfer/moe/_shared/kernels/w4a16/prepare.py (1)

1855-1882: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Device comparison here is looser than the gate_suh/up_suh check.

Line 1861 tests resolved_device.index is None instead of the supplied tensor's index, so an indexless resolved_device (e.g. torch.device("cuda") from the synthetic path) accepts a dummy_scale on any CUDA device. Aligning with the strict comparison used at Line 1839 would keep the zero-copy binding contract uniform.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sparkinfer/moe/_shared/kernels/w4a16/prepare.py` around lines 1855 - 1882,
Update the dummy_scale device validation in the surrounding preparation function
to use the same strict device-type-and-index comparison as the gate_suh/up_suh
validation, including when resolved_device.index is None. Do not allow an
indexless resolved_device to accept dummy_scale from an arbitrary device;
preserve the existing dtype, shape, contiguity, and alignment checks.
sparkinfer/_lib/compiler.py (1)

1465-1475: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Arch key is captured from whichever device is current at first call.

get_device_capability()/get_device_name() with no argument read the current device, and the result is memoized process-wide. If the key is computed before torch.cuda.set_device(...) (or on a heterogeneous-GPU node), the on-disk cache can be keyed by a different device than the one actually compiled for. Consider binding to the explicitly selected device and, optionally, keying per device index.

♻️ Suggested hardening
-        major, minor = torch.cuda.get_device_capability()
-        name = torch.cuda.get_device_name()
+        index = torch.cuda.current_device()
+        major, minor = torch.cuda.get_device_capability(index)
+        name = torch.cuda.get_device_name(index)

As per coding guidelines, "Migration acceptance must be GPU-only and exercise the real production launch path on explicitly selected physical GPUs".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sparkinfer/_lib/compiler.py` around lines 1465 - 1475, Update the
device-architecture key computation around torch.cuda.get_device_capability and
torch.cuda.get_device_name to use the explicitly selected physical GPU rather
than the implicit current device, and include the device index in the
memoization/cache key when devices may differ. Preserve the existing unavailable
fallback, and ensure migration acceptance exercises the production launch path
on explicitly selected GPUs only.

Source: Coding guidelines

tests/attention/test_fused_indexer.py (1)

549-575: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make the high-page boundary an executable invariant.

Add assert pid_lo * record_bytes >= (1 << 31) near the fixture constants. Otherwise, a future stride or page-ID change could leave this test passing without exercising the intended Int32-offset boundary.

As per coding guidelines, paged-pool tests must include live pages beyond the 2^31 / stride boundary.

Suggested guard
     pid_lo, pages_used = 2000, 64
     pool_pages = pid_lo + pages_used + 4
     need = pool_pages * record_bytes + (1 << 29)
+    assert pid_lo * record_bytes >= (1 << 31)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/attention/test_fused_indexer.py` around lines 549 - 575, Add an
executable assertion near the fixture constants requiring pid_lo * record_bytes
to be at least 1 << 31. Keep the existing high-page setup unchanged so the
paged-pool test continues exercising live pages beyond the Int32 offset
boundary.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/attention/test_fused_indexer.py`:
- Around line 589-605: Update the validation around _golden_topk to return
ordered golden indices alongside golden values. For each row, use the returned
idx[row] to gather the corresponding expected golden values, then compare those
values and indices pairwise against the implementation output; remove the
independent sorted-value and set-only assertions.

---

Nitpick comments:
In `@sparkinfer/_lib/compiler.py`:
- Around line 1465-1475: Update the device-architecture key computation around
torch.cuda.get_device_capability and torch.cuda.get_device_name to use the
explicitly selected physical GPU rather than the implicit current device, and
include the device index in the memoization/cache key when devices may differ.
Preserve the existing unavailable fallback, and ensure migration acceptance
exercises the production launch path on explicitly selected GPUs only.

In `@sparkinfer/moe/_shared/kernels/w4a16/prepare.py`:
- Around line 1855-1882: Update the dummy_scale device validation in the
surrounding preparation function to use the same strict device-type-and-index
comparison as the gate_suh/up_suh validation, including when
resolved_device.index is None. Do not allow an indexless resolved_device to
accept dummy_scale from an arbitrary device; preserve the existing dtype, shape,
contiguity, and alignment checks.

In `@tests/attention/test_fused_indexer.py`:
- Around line 549-575: Add an executable assertion near the fixture constants
requiring pid_lo * record_bytes to be at least 1 << 31. Keep the existing
high-page setup unchanged so the paged-pool test continues exercising live pages
beyond the Int32 offset boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be21eac0-1684-4c5f-8446-eedb7b38aef9

📥 Commits

Reviewing files that changed from the base of the PR and between 5dd6f15 and 419cb1e.

📒 Files selected for processing (19)
  • pyproject.toml
  • sparkinfer/__init__.py
  • sparkinfer/_lib/compiler.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/__init__.py
  • sparkinfer/moe/_shared/kernels/w4a16/host.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py
  • sparkinfer/moe/_shared/kernels/w4a16/prepare.py
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • sparkinfer/moe/trellis_moe/__init__.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/moe/trellis_moe/api.py
  • tests/_lib/test_compile_cache.py
  • tests/attention/test_fused_indexer.py
  • tests/moe/test_trellis_moe.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • validation/cutlass_migration/evidence/kernel_resources.py
🚧 Files skipped from review as they are similar to previous changes (16)
  • sparkinfer/init.py
  • sparkinfer/moe/trellis_moe/api.py
  • pyproject.toml
  • sparkinfer/moe/_shared/kernels/w4a16/route_pack.py
  • sparkinfer/moe/trellis_moe/init.py
  • validation/cutlass_migration/acceptance/e2e/contract.py
  • sparkinfer/moe/_shared/kernels/w4a16/init.py
  • tests/_lib/test_compile_cache.py
  • sparkinfer/moe/init.py
  • validation/cutlass_migration/acceptance/corpus/ptx_capture.py
  • validation/cutlass_migration/evidence/kernel_resources.py
  • sparkinfer/moe/_shared/kernels/w4a16/host.py
  • tests/moe/test_trellis_moe.py
  • sparkinfer/moe/trellis_moe/_impl.py
  • sparkinfer/_lib/intrinsics.py
  • sparkinfer/moe/_shared/kernels/w4a16/kernel.py

Comment thread tests/attention/test_fused_indexer.py Outdated
The direct-K high-page-id test compared the sorted top-k values and the
returned index set independently, so a regression that emitted the correct
values against the wrong indices would still pass.

_golden_topk grows an opt-in return_scores flag (existing call sites keep the
two-tuple contract) and the test now gathers the golden score for each index
the kernel actually returned and compares it position by position. Gathering
by the returned index instead of asserting an index order keeps the check
robust to ties at the top-k boundary.
…est invariant

compiler: read the arch cache key from the explicitly selected device.
torch.cuda.get_device_capability()/get_device_name() with no argument read the
implicitly current device and the result is memoized process-wide, so a lookup
made before a worker selects its GPU pins the key to the wrong card. Observable
on mixed rigs: Max-Q and non-Max-Q RTX PRO 6000 boards share compute capability
12.0 but report different device names.

prepare: normalise an indexless resolved_device to a concrete ordinal in the
synthesize path, then validate dummy_scale with the same strict comparison used
for the suh/svh tables. Previously the two disagreed -- suh/svh used strict
equality (so cuda:0 != cuda rejected a correctly placed tensor) while
dummy_scale used an index-optional comparison that accepted a tensor from any
ordinal. Normalising once makes both strict and correct.

tests: assert the direct-K case actually crosses the Int32 page-offset boundary.
The i64 path it exists to cover is implied only by the constants, so retuning
record_bytes or pid_lo downward would leave a passing test that no longer
exercises the boundary.
@brandonmmusic-max

Copy link
Copy Markdown
Author

@coderabbitai All three nitpicks addressed in 72d7434. Two of them were more than cosmetic on a mixed-GPU rig, so thanks.

_lib/compiler.py — arch key from the implicit current device. Fixed as suggested: the capability/name lookups now take an explicit torch.cuda.current_device() index. This one is real rather than theoretical here — the validation rig has four RTX PRO 6000 boards where two are Max-Q and two are not. They share compute capability 12.0 but report different get_device_name() strings, so a key memoized from whichever device happened to be current before the worker selected its GPU could name the wrong card. Since the stated purpose of this function is to stop a cubin built for one architecture being served to another, the implicit lookup undercut the guarantee it exists to provide.

I deliberately did not add the device index to the cache key itself. All ranks on a homogeneous-capability rig should keep sharing compiled artifacts; keying on the ordinal would fragment the JIT cache per rank and multiply cold-start compile time for no correctness gain.

prepare.py — dummy_scale device check. Fixed, but not by only tightening that comparison. The two checks disagreed in both directions: resolved_device can legitimately be indexless (the synthesize path builds it from a caller-supplied device, so torch.device("cuda") is valid), and in that case the strict suh/svh check scale.device != resolved_device would reject a correctly placed tensor because cuda:0 != cuda, while dummy_scale accepted a tensor from any ordinal. Tightening dummy_scale alone would have left the over-strict suh/svh path intact.

Instead the synthesize path now pins an indexless resolved_device to a concrete ordinal once, up front. The native path already derives it from w13.device, so by the time either validation runs the device always carries an index — and dummy_scale now uses the same strict equality as suh/svh. One normalisation makes both checks strict and correct.

test_fused_indexer.py — high-page boundary. Added assert pid_lo * record_bytes >= (1 << 31) with a message explaining what regresses if it trips. Worth noting the current margin is thin: 2,154,240,000 vs 2,147,483,648, about 6.7 MB. Retuning record_bytes or pid_lo even slightly downward would have left a green test that no longer exercised the i64 path at all — the same silently-useless-test failure mode as the value/index pairing issue you caught earlier.

All three verified by syntax check and the boundary arithmetic above. The CUDA suite has not been run against this head from CI.

@brandonmmusic-max

Copy link
Copy Markdown
Author

Runtime bumped to v24 (target/draft scratch isolation)

The validated runtime referenced in this PR is now:

verdictai/glm52-exl3-sparkinfer:v24-gg-v20final-scopefix-vllm5517197-sibe0edca-cu132-sm120a@sha256:8753406fd7a05693c314e3f7b08df5c338bdeb548b1e487b7865a465e6b480b9

It supersedes the earlier v23 image, which predates the rank-sliced runtime scoping fix. The pins in the description are updated accordingly.

Why: the EXL3 rank-sliced runtime cache was keyed only on device/dtype/shape/topk/planner settings, and the cached entry owns mutable Trellis/prefill scratch. The MTP-78 draft layer matches a target MoE layer on every one of those components, so it was reusing the target's scratch. The key is now scoped to the owning quant config, giving each model its own runtime.

Measured effect (the fix is visible in memory, not in the log -- the planner line uses info_once and is deduplicated):

Rank-sliced runtimes GPU KV cache Concurrency @524K
v23 (shared) 1 (target+draft collide) 1,115,904 tokens 2.13x
v24 (scoped) 2 (isolated) 996,352 tokens 1.90x

The 119,552-token KV reduction corresponds to ~1056 MiB against a 1054.2 MiB arena, which is direct evidence that a second runtime is allocated. The reduced KV capacity is the intended cost of the isolation, and it is also why the fix is scoped per model rather than per layer -- per-layer ownership would have needed tens of GiB per rank across 75+ layers.

Quality on v24: Estonia c2 x5 = 5/5 pass. LAVD c5 across two independent 5-run samples = 4 EXACT / 5 NEAR / 1 FAIL over 10 runs (sample 1: 2E/3N/1F, sample 2: 2E/3N/0F, matching the pre-fix image exactly). The single miss was a wrong ledger total at 7,114 completion tokens against a 24,576 cap, so answer quality rather than truncation, and within this profile's historical spread.

Reporting the aggregate including the failure rather than only the clean sample.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format='commit %H%n%s' --stat 72d7434

printf '\n--- relevant declarations ---\n'
ast-grep outline sparkinfer/_lib/compiler.py --items all --type function --match 'device_arch'
ast-grep outline sparkinfer/moe/_shared/kernels/w4a16/prepare.py --items all --type function --match 'prepare_trellis256_moe_weights|prepare_trellis256_dense_weight'
ast-grep outline tests/attention/test_fused_indexer.py --items all --type function --match 'high_page_id'

printf '\n--- compiler device-arch implementation ---\n'
rg -n -C 12 '_device_arch_key|current_device|get_device_capability|get_device_name' sparkinfer/_lib/compiler.py

printf '\n--- trellis preparation device normalization/validation ---\n'
rg -n -C 10 'resolved_device|dummy_scale|gate_suh|up_suh|svh|cuda' sparkinfer/moe/_shared/kernels/w4a16/prepare.py

printf '\n--- high-page boundary assertion ---\n'
rg -n -C 12 'pid_lo|record_bytes|1 << 31|high_page_id_i64' tests/attention/test_fused_indexer.py

printf '\n--- targeted commit diff ---\n'
git show --no-ext-diff --format= -- \
  72d7434 \
  sparkinfer/_lib/compiler.py \
  sparkinfer/moe/_shared/kernels/w4a16/prepare.py \
  tests/attention/test_fused_indexer.py

Length of output: 31151


@brandonmmusic-max Thanks—static verification confirms the prepare.py normalization/device-equality fix and the high-page Int32-boundary invariant are correctly implemented. The latter now fails loudly if future constants cease to exercise the Int64 offset path.

One correction on _device_arch_key(): this change does not resolve the memoization-order concern described. torch.cuda.get_device_capability() and torch.cuda.get_device_name() without an argument already query torch.cuda.current_device(). Passing that same ordinal explicitly is equivalent, so _DEVICE_ARCH_KEY can still be populated from whichever device is current on the first call.

To guarantee the intended behavior on a mixed-GPU process, the architecture key needs to be derived from an explicit target/compile device propagated to this code path (and cached per architecture identity), or cache initialization must be deferred until after device selection. As written, this finding remains unresolved.

The previous change was not a fix. torch resolves get_device_capability() and
get_device_name() against torch.cuda.current_device() when called without an
argument, so passing that same ordinal explicitly is equivalent and left the
process-wide _DEVICE_ARCH_KEY free to freeze whichever GPU happened to be
current on the first call. It also broke
test_device_arch_key_retries_after_unavailable, whose torch stubs take no
device argument.

Replace the single global with a per-ordinal dict so each entry is bound to the
device it was measured on, and thread the ordinal through
_static_compile_cache_context, which is lru_cached on compile_callable and
would otherwise re-freeze the identity at that layer regardless.

The returned key still omits the ordinal: two GPUs with the same capability and
name produce the same key and keep sharing compiled artifacts, so a homogeneous
rig does not fragment its JIT cache. Only the lookup is per ordinal.

Adds test_device_arch_key_is_memoized_per_ordinal, which models a mixed rig
(two boards sharing compute capability 12.0 but reporting different names) and
asserts the second board does not inherit the first board's identity. Restores
the retry test against the per-ordinal cache.
@brandonmmusic-max

Copy link
Copy Markdown
Author

@coderabbitai You're right, and thanks for not letting that one go. My previous change was not a fix — torch.cuda.get_device_capability() / get_device_name() already resolve against torch.cuda.current_device() when called with no argument, so passing that same ordinal explicitly is semantically identical and left _DEVICE_ARCH_KEY free to freeze whichever GPU was current on the first call. I over-claimed; the finding was genuinely unresolved.

It was also worse than a no-op: it broke test_device_arch_key_retries_after_unavailable, whose torch stubs are zero-argument lambdas, so the explicit-index call raised TypeError, got swallowed by the except, and returned ("arch", "unavailable"). I confirmed that failure locally before fixing it.

Fixed properly in d4438d4:

Per-ordinal memoization. _DEVICE_ARCH_KEY (single global) is replaced by _DEVICE_ARCH_KEYS: dict[int, tuple]. Each entry is bound to the ordinal it was measured on, so selecting a different device re-probes instead of returning a stale identity. _device_arch_key() also accepts an explicit ordinal for callers that know their compile target.

Second cache layer, which your comment led me to. _device_arch_key() is consumed inside _static_compile_cache_context, which is @lru_cached on compile_callable alone — so fixing only the inner function would still have frozen the architecture identity at that layer for a given callable. The ordinal is now part of that lru_cache key too, and the caller passes _current_device_ordinal().

Deliberate non-change: the returned key still omits the ordinal. Two GPUs with the same capability and name produce the same key and keep sharing compiled artifacts; keying on the ordinal would fragment the JIT cache per rank and multiply cold-start compiles for no correctness gain on a homogeneous rig. Only the lookup is per ordinal.

Tests: added test_device_arch_key_is_memoized_per_ordinal, which models a mixed rig — two boards sharing compute capability 12.0 but reporting different names — and asserts the second board does not inherit the first's identity, that an explicit ordinal resolves independently of the current device, and that both entries coexist in the cache. The retry test is restored against the per-ordinal cache and additionally asserts an unavailable probe is never memoized.

tests/_lib/test_compile_cache.py: 6 passed. Wider tests/_lib/: 11 passed, 1 failure in test_cutlass_runtime_patches.py::test_patches_apply_by_default that I verified is pre-existing — it reproduces identically with my changes stashed, as it needs a real CUTLASS runtime.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --format='commit %H%n%s' --stat d4438d4

printf '\n--- compiler function map ---\n'
ast-grep outline sparkinfer/_lib/compiler.py --items all --type function --match 'device_arch|static_compile_cache_context|compile_disk_cache_payload'

printf '\n--- compiler implementation and call sites ---\n'
rg -n -C 10 '_DEVICE_ARCH_KEYS|_DEVICE_ARCH_KEY|_current_device_ordinal|_device_arch_key|_static_compile_cache_context' sparkinfer/_lib/compiler.py

printf '\n--- targeted test map ---\n'
ast-grep outline tests/_lib/test_compile_cache.py --items all --type function --match 'device_arch|disk_cache_key'

printf '\n--- targeted test implementations ---\n'
rg -n -C 12 'test_device_arch_key_retries_after_unavailable|test_device_arch_key_is_memoized_per_ordinal|_DEVICE_ARCH_KEYS|device_arch' tests/_lib/test_compile_cache.py

printf '\n--- focused commit diff ---\n'
git show --no-ext-diff --format= d4438d4 -- \
  sparkinfer/_lib/compiler.py \
  tests/_lib/test_compile_cache.py

Length of output: 19918


@brandonmmusic-max Confirmed—d4438d4 resolves the finding.

_device_arch_key() now memoizes successful probes per device ordinal, and _static_compile_cache_context() also keys its LRU cache by ordinal. _compile_disk_cache_payload() supplies the current ordinal, so switching devices can no longer reuse an architecture identity captured for the same callable on a different GPU.

The returned semantic key appropriately remains based on architecture identity rather than ordinal, retaining artifact sharing across equivalent GPUs. The focused tests cover unavailable-probe retry, mixed-device identities, explicit ordinal lookup, and simultaneous cache entries for both devices.


✏️ Learnings added
Learnt from: brandonmmusic-max
URL: https://github.com/local-inference-lab/sparkinfer/pull/49

Timestamp: 2026-07-25T23:24:24.560Z
Learning: In `sparkinfer/_lib/compiler.py`, CUDA architecture identity is memoized per device ordinal in `_DEVICE_ARCH_KEYS`, while the returned cache identity intentionally omits the ordinal so equivalent GPUs share compiled artifacts. `_static_compile_cache_context()` must include the ordinal in its LRU-cache key and callers must pass the compile target’s current ordinal; otherwise an architecture identity can become stale on mixed-GPU processes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@brandonmmusic-max

Copy link
Copy Markdown
Author

Runtime bumped to v26 — supersedes the v24 note above

verdictai/glm52-exl3-sparkinfer:v26-gg-v20final-scopefix-archkey-vllm5517197-sibe0edca-cu132-sm120a@sha256:2bb9e804a283d1da3b7e3425ff87375121285141d0d0a40d3dc09d41bf881a10

The description pins have been updated to this image. v24 and the intermediate v25 are superseded — please disregard both.

What changed since v24. v24 carried the per-model rank-sliced runtime scoping (target/draft scratch isolation). v25 additionally carried a change to the compile-cache architecture key that CodeRabbit correctly identified as a no-op: torch.cuda.get_device_capability() / get_device_name() already resolve against torch.cuda.current_device(), so passing that ordinal explicitly changed nothing, and the process-wide key could still freeze whichever GPU was current on the first call. That attempt also broke test_device_arch_key_retries_after_unavailable, whose torch stubs take no device argument.

v26 carries the real fix:

  • the architecture key is memoized per device ordinal, so each entry is bound to the device it was measured on;
  • the ordinal is threaded through _static_compile_cache_context, which is lru_cached on the compile callable and would otherwise have re-frozen the identity at that layer regardless;
  • the returned key still omits the ordinal, so GPUs of the same architecture keep sharing compiled artifacts rather than fragmenting the JIT cache per rank.

This is not academic on the validation rig: its four boards report two different device names (Max-Q and non-Max-Q) while sharing compute capability 12.0.

Validation on v26 (4x RTX PRO 6000 SM120a, TP4/DCP4, MTP-3, NVFP4 DS-MLA KV):

Suite Config Result
Estonia c2, 5 runs 5/5 pass, 0 fail, correct rate 1.00
LAVD c5, 5 runs 3 EXACT / 2 NEAR / 0 FAIL, correct rate 1.00

GPU KV cache 998,656 tokens (1.90x at 524,288). That is ~1 GiB below the pre-isolation 1,115,904 (2.13x), which is the intended cost of giving the draft model its own Trellis/prefill arena.

Unit tests: tests/_lib/test_compile_cache.py 6 passed, including a new case that models a mixed rig and asserts the second board does not inherit the first board's architecture identity. The one failure elsewhere in tests/_lib/ reproduces with these changes stashed and is pre-existing.

CI status unchanged: pre-run-check still fails on this repo's label gate, so the test suite has not run against this head. Everything above is local validation.

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.

1 participant