Skip to content

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

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

moe: add planned EXL3 Trellis path for fused Blackwell MoE#49
brandonmmusic-max wants to merge 11 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 added a commit to brandonmmusic-max/b12x that referenced this pull request Jul 25, 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 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
@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.

brandonmmusic-max pushed a commit to brandonmmusic-max/vllm that referenced this pull request Jul 26, 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 26, 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 the new SparkInfer v20 base (e603f74) and added small-m numerical coverage for the planned full-rotation Trellis path, prompted by vLLM #183.

Why

#183 reported that widening the Trellis window (VLLM_EXL3_TRELLIS_MIN_M=1), so an MTP-N draft's m=1..N GEMMs stay on the fused path instead of falling to the eager parity path, silently corrupts long-context output while short prompts pass.

The claim did not reproduce — but it exposed a real coverage hole. The entire trellis3_t256 + full_rotation production path had exactly one numerical test, and it ran m == max_tokens == 2 with topk=2 on a non-production tile. Nothing covered m < plan capacity, which is precisely the window-widening regime. At m < moe_block_size the route packer emits mostly-padding blocks (at m=3, topk=8 that is roughly 1–3 real rows out of 8), so padding masking is load-bearing and was untested.

What was added

test_planned_full_rotation_small_m_partial_blocks, parametrized over m ∈ {1,2,3,4,8} at production geometry (tile 64x256x64x256, block_size_m=8, capacity 32, topk=8, 32 experts, bf16 in), with three oracles:

  1. Per-token bitwise equality against an m=1 run of the same token. A token's output depends only on its own routes, so batch composition must not change a single bit.
  2. NaN-poisoned arena — the scratch buffer is filled with 0xFF before every bind, so any read of a padding or uninitialised slot surfaces as NaN instead of as plausible numbers.
  3. m strictly below plan capacity, the regime the issue implicates.

All five cases pass. Padding masking, sentinel handling and uninitialised-scratch behaviour are correct at m=1,2,3 — this converts "we believe MIN_M=1 is safe" into a gate rather than an assumption.

Corroborating end-to-end result

On 4x RTX PRO 6000 (SM120), TP4/DCP4, nvfp4_ds_mla KV, MTP-3 draft at layer 78, MIN_M=1: needle-in-a-haystack recovered 24/24 at three depths across 8k → 480k target context, up to 479,396 real prompt tokens, with no garbled output and zero engine restarts.

Note for anyone chasing #183: MIN_M=1 widens the window so small-m draft GEMMs remain fused and graph-capturable; it is not a per-token capture and is not a throughput tax. The remaining untested surface is cudagraph capture at m < plan capacity — the upstream capture test covers m == capacity only.

brandonmmusic-max pushed a commit to brandonmmusic-max/vllm that referenced this pull request Jul 27, 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 27, 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 27, 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 master after #79 ("perf(pcie): add exact DCP top-k owner exchange") landed — clean, 11 commits, no overlap (this PR touches the MoE/Trellis lane; #79 adds comm/pcie/ files and one packaging-test line).

Verified on the rebased branch: the Trellis small-m + capture-below-capacity tests pass (8/8), and #79's own CPU contract tests pass here too (5/5). Downstream note: the wheel shipped in verdictai/glm52-exl3-sparkinfer:v30-…@sha256:f13f2f38… is built from this branch, so it now carries the #79 module as a strict superset of master — dormant until the vLLM-side owner algorithm lands in a base.

brandonmmusic-max and others added 11 commits July 27, 2026 12:53
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>
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.
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.
…lis path

vLLM issue #183 reported that widening the Trellis window
(VLLM_EXL3_TRELLIS_MIN_M=1), so an MTP-N draft's m=1..N GEMMs stay on the fused
path instead of falling to the eager parity path, silently corrupts output at
long context while short prompts pass.

The claim was not reproducible, but it exposed a real coverage hole: the entire
trellis3_t256 + full_rotation production path had exactly one numerical test,
and it ran m == max_tokens == 2 with topk=2 on a non-production tile. Nothing
covered m < plan capacity -- which is precisely the window-widening regime --
and at m < moe_block_size the route packer emits mostly-padding blocks (at m=3,
topk=8, roughly 1-3 real rows out of 8), so padding masking is load-bearing.

Adds a parametrized test over m in {1,2,3,4,8} at production geometry
(tile 64x256x64x256, block_size_m=8, capacity 32, topk=8, 32 experts) with
three oracles:

  * per-token bitwise equality against an m=1 run of the same token, since a
    token's output depends only on its own routes and must not change with
    batch composition;
  * the scratch arena pre-filled with 0xFF (NaN in fp32/fp16) before every
    bind, so any read of a padding or uninitialised slot surfaces as NaN
    rather than as plausible numbers;
  * m strictly below plan capacity, the regime the issue implicates.

All five cases pass, which converts "we believe MIN_M=1 is safe" into a gate.
Closes the last surface left untested by vLLM #183. The existing capture test
captures at m == max_tokens, and the eager small-m test does not cover capture:
bind() runs inside the captured region, so the route-pack grid, the
kernel_workspace zero-fill and the active-m constants are baked into the graph.
If any were captured for the wrong m, replay would return stale or mismatched
results while eager execution stayed correct -- which is precisely a "short
prompts pass, long context corrupts" signature once an MTP-N draft replays its
captured graph.

Parametrized over m in {1,2,3} with capacity 32 at production geometry. Asserts
replay matches eager bit-for-bit, no NaN after replay, and that a second replay
is stable (no dependence on scratch left over from the capture pass).

All three pass, so capture at m below capacity is not the reported defect
either.
@lukealonso

Copy link
Copy Markdown
Collaborator

Thanks Brandon — the kernel-level work here is excellent, and the validation story (graph-capture tests, small-m partials, the production-layer oracle at 8.7e-4 rel error) is exactly what I want to see. The intrinsics, the trellis3_t256 kernel layout, the full-rotation dataflow, the zero-copy prepare, and the device-arch cache-key fix should all land essentially as-is.

I want to push back on one design decision before this merges, though: the new public sparkinfer.moe.trellis_moe op. EXL3 trellis should be another weight format under the existing MoE API, not a new API surface. The PR's own layering says so:

  • The shared kernel models it that way already: _WEIGHT_LAYOUTS = {"packed", "modelopt", "nf3_2p1", "trellis3_t256"}. nf3_2p1 — a different codebook, packing, and scale semantics — ships today as a weight layout under fused_moe's w4a16 recipe, per fused_moe's own docstring ("Recipes are arguments, not separate ops"). Trellis is the fourth entry in the same enum, executed by the same run_w4a16_moe.
  • trellis_moe/_impl.py contains no compute. plan calls plan_w4a16_buffers/compile_w4a16_fused_moe, run forwards kwargs to run_w4a16_moe, clear_caches clears the shared w4a16 cache, and TrellisMoEWeights._prepared is literally a PreparedNF3MoeWeights. It's ~775 lines of validation and arena bookkeeping around machinery another op owns.
  • There's no lifecycle mismatch to justify it: fused_moe is also api_style="planned" with the identical verb set (Caps/plan/bind/run, plan_weights/prepare_weights, scratch_specs(), caller-owned scratch, capture-safe run).
  • Our MoE op taxonomy is split by execution strategy (fused_moe = fused TP, ep_moe = expert parallel), and both already carry w4a16 as a recipe. trellis_moe is the first op keyed by format while reusing the fused-TP strategy — a precedent we'd have to either follow (an op per format, forever) or walk back later at higher cost.
  • Everything genuinely new is parameter-shaped: four rotation tables (weight-lifecycle kwargs), FP32 output + fp16 element path + pinned block_size_m + H/I % 128 (Caps/plan fields), silu-only (an activation gate). The PR demonstrates this itself by pushing full_rotation into the shared plan_w4a16_buffers and kernel rather than forking them — only the outermost wrapper was forked.

The cost is already visible in the diff: duplicated arena/validation machinery, two clear_caches for one cache, a hard-coded activation="silu" gate that diverges from fused_moe's activations-as-arguments contract (the Kimi-K3 SiTU bring-up has already had to patch that gate downstream — the drift is real, not hypothetical), and _shared/kernels/w4a16/__init__.py now declaring "Public preparation and execution surfaces" inside a private-by-convention package because the dense helpers had no proper home.

I think the root cause is pre-existing debt on our side: the w4a16 recipe was never finished as a first-class fused_moe recipe (_get_activation_kernel_spec still raises "W4A16 dispatch uses sparkinfer.moe._shared.kernels.w4a16.kernel directly", and consumers reach it via the sparkinfer::w4a16_fused_moe_launch custom op registered in the private kernel module). Routing around that seam was the path of least resistance — but it converts one leaky abstraction into two public surfaces plus the leak.

Proposed resolution plan

Phase 0 — split the independent fixes out (small PRs, land first):

  1. The compiler device-arch cache-key fix + tests/_lib/test_compile_cache.py. It fixes cross-GPU cubin aliasing for every op and shouldn't be coupled to trellis review.
  2. The fused-indexer i64 high-page-id regression test and the validation/ metadata updates (unrelated to MoE; the test itself is nicely designed — asserting the offset actually overflows Int32 is the right way to keep it honest).

Phase 1 — land the compute layers unchanged (this PR, trimmed):

  • _lib/intrinsics.py trellis dequant family (3/4/5/6 bpw, fp16/bf16)
  • w4a16/kernel.py trellis3_t256 layout, full-rotation dataflow, FP32 top-k sum launches
  • w4a16/prepare.py prepare_trellis256_moe_weights + dense prepare
  • w4a16/host.py full_rotation/block_size_m planning, route_pack.py graph-safe expert_counts count+prefix

Phase 2 — expose trellis through fused_moe instead of a new op:

  1. Weight lifecycle: extend plan_weights/prepare_weights with source_format="exl3_trellis_mcg" (validating the MCG sentinel exactly as _validate_mcg does now) and the rotation-table kwargs (gate_suh, up_suh, intermediate_rotations, down_svh). Wrap the result in the existing ExpertWeights union rather than a new Weights type. Rename PreparedNF3MoeWeightsPreparedW4A16MoeWeights since it now carries three formats.
  2. Caps: add the trellis fields (trellis_bits, tile_config, explicit block_size_m) as optional Caps parameters — or a per-recipe params block if we'd rather not widen Caps — validated exactly as TrellisMoECaps.__post_init__ does today.
  3. Plan/bind: fold the full-rotation buffer inventory (rotation_a_gate/rotation_a_up, FP32 output view, expert_counts) into TPMoEScratchPlan instead of the parallel _ArenaLayout, so there is one arena layout implementation and one binding path.
  4. Finish w4a16 as a real recipe while we're in there: remove the _get_activation_kernel_spec bypass and take activations from Caps like every other recipe, so silu/situ/relu2 gating lives in one place. This retires the debt that motivated the fork and is precisely where the SiTU patch wants to live.
  5. Dense trellis: give run_trellis256_dense/prepare_trellis256_dense_weight a public home (a gemm-group op, or a dense entry on fused_moe) and revert _shared/kernels/w4a16/__init__.py to private.
  6. Tests: port tests/moe/test_trellis_moe.py to the fused_moe surface (the correctness/capture cases carry over unchanged), and add one cross-recipe test asserting nf3_2p1 and trellis3_t256 flow through the same plan/bind/run path — that's the regression guard for this whole discussion.

Phase 3 — consumer migration:

  • trellis_moe is unreleased, so I'd prefer we drop it from this PR and rebase vllm#139 onto the fused_moe surface — one rebase now versus a permanently doubled public MoE API. If the GLM-5.2 release schedule makes that painful, the fallback is landing trellis_moe as a thin shim that delegates to the fused_moe path, marked deprecated-on-arrival in OpMeta.notes, with removal tracked for the next minor. What I don't want is the shim quietly becoming load-bearing.

Two small correctness nits to pick up in the same pass, wherever the surface ends up:

  • is_supported claims "SM12x Trellis kernel stack" but only checks requires=("triton",) — it returns True on hardware where plan-time compile will fail. Gate on compute capability or soften the docstring.
  • bind zeroes kernel_workspace outside graph capture. The capture tests passing implies the kernel self-resets the workspace per launch — if that's the invariant, a one-line comment at the zero_() would keep someone from "optimizing" replay into a stale-state bug later.

Happy to pair on the Phase 2 plumbing — the hard part (kernel + prepare + validation) is already done and done well; this is about where the door is, not what's behind it.

Drafted with Claude Code assistance; reviewed before posting.

@voipmonitor

Copy link
Copy Markdown
Contributor

Superseded by #90, which retains the Trellis intrinsics, native preparation, full-rotation W4A16 kernels, graph-safe routing, and dense path, but exposes EXL3 Trellis as source_format="exl3_trellis_mcg" through the existing sparkinfer.moe.fused_moe plan/bind/run lifecycle instead of adding a parallel public trellis_moe operator and scratch arena. The independent compiler cache-key correction and high-i64 indexer test were split into #88 and #89 respectively. The paired vLLM consumer remains #190 and is being updated to the unified API.

voipmonitor pushed a commit to voipmonitor/vllm that referenced this pull request Jul 29, 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)
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.

3 participants