Conversation
…ntion, Markov head)
NSagan271
left a comment
There was a problem hiding this comment.
Some blocking issues, especially some correctness issues in the speculative decoding resource path and the fact that a combination of inline chunks and tensors read through transport can result in out-of-order outputs.
| return mode.strip().lower() in ("auto", "flashinfer") | ||
|
|
||
|
|
||
| class CommGroup: |
There was a problem hiding this comment.
This class now includes three different implementations of all-reduce; it works but is not the most modular and readable. I would recommend moving some of the logic (the all-reduce itself, as well as logic for when each method is applicable) to a new abstract Communicator class with subclasses for SymmMem, NCCL, and Lamport (or for NCCL and SymmMem, with the Lamport logic in SymmMem, which may be technically more correct).
| @@ -0,0 +1,95 @@ | |||
| """Coalesce per-request tensor operations that touch one underlying storage. | |||
|
|
|||
| A batched forward hands the engine one tensor per request, usually a slice of one batch-wide | |||
There was a problem hiding this comment.
@merceod how do you plan to compose this with the BatchedModelOutput I added in #258? I do like that the BatchedModelOutput makes exposes this behavior to the model implementor (e.g., they won't see a performance dip if they actually return something non-contiguous; they also can return a single tensor instead of splitting by rid), but it is also nice that the StorageSpan objects are created automatically from whatever any model returns.
| self._kv_len_buf: torch.Tensor | None = None # the rows' planned kv lengths, on device | ||
| on_gpu = torch.device(device).type == "cuda" | ||
| self.fallback = not on_gpu or not flashinfer_mla_supports(kv_lora_rank, qk_rope_head_dim) | ||
| if self.fallback: |
There was a problem hiding this comment.
The fallback is not cuda graph compatible, so I'd prefer to fail loudly here if use_cuda_graph is True, instead of waiting for an error during cuda graph capture itself.
| # [layer, page, token, ckv | kpe]; the attention kernel takes the two | ||
| # column slices as strided views (page/token strides are explicit | ||
| # kernel params, only the last dim must be contiguous) | ||
| self.tensor = torch.zeros( |
There was a problem hiding this comment.
The CPU page pool currently allocates its tensor based on the default KVLayout.NHD. It should be changed to take the dimensions of the real KV cache (after the first two dimensions, which are config.num_layers, and max_cpu_pages).
| from mstar.distributed.communication import CommGroup | ||
| from mstar.distributed.utils import divide | ||
|
|
||
| COLUMN = "column" |
There was a problem hiding this comment.
Nit: can use an enum here
|
|
||
| key = (shape, dtype, device) | ||
| ring = self._symm_bufs.get(key) | ||
| if ring is None: |
There was a problem hiding this comment.
Every new shape allocates a new buffer; this is fine for cuda graph mode because the number of shapes is finite, but will keep on allocating new buffer for eager mode.
| # warmup runs each capture shape eagerly first, so this never happens | ||
| # inside a CUDA-graph capture. | ||
| bufs = [] | ||
| for _ in range(self._SYMM_RING if self._symm_multimem else 1): |
There was a problem hiding this comment.
Would _symm_multimem == False ever be a correctness issue; e.g., for BAGEL, where the MoT has two different output projections?
| if not os.path.exists(lock_path): | ||
| return | ||
| try: | ||
| if time.time() - os.path.getmtime(lock_path) < min_age_s: |
There was a problem hiding this comment.
This could still cause a hang if a server dies while holding the lock and is restarted before 20 minutes. Claude's recommendation is (take it with a grain of salt):
Put our own fcntl.flock around load(). Use a sidecar file, e.g. <build_dir>/mstar.flock.
The kernel drops a flock when the holder dies, including on SIGKILL. On NFS, Linux turns flock into a POSIX lock that the server drops when the dead client's lease runs out. So the lock can't be left behind, even across nodes.
While holding it, any torch lock file you find must be stale, because every mstar loader goes through the wrapper. You can delete it unconditionally, and both the /proc scan and the 20-minute check go away.
One helper would cover align.py and marlin/init.py.
Caveat: on an NFS mount with nolock, flock only works within a node. That is no worse than today's /proc scan.
| did_work = True | ||
| result = self.result_tensor_queue.get() | ||
| if isinstance(result, InlineResult): | ||
| self._deliver_inline_result(result) |
There was a problem hiding this comment.
If there are ever inline tensors queued after larger chunks, the inline tensors can get sent to the client while the previous chunks are still being read, resulting in an out-of-order output. This is not relevant to Kimi because all output chunks are small and can be inlined, but would be relevant for an audio model that has chunk-to-chunk variability.
| load( | ||
| name="_mstar_marlin_C", sources=sources, is_python_module=False, verbose=False, | ||
| extra_include_paths=[_CSRC, _MOE], extra_cflags=["-O3", "-std=c++17"], | ||
| extra_cuda_cflags=["-O3", "-std=c++17", "-gencode", "arch=compute_90,code=sm_90", |
There was a problem hiding this comment.
Does the arch=compute_90,code=sm_90" block the marlin kernels from building on Blackwell? If so marlin_supports in mstar/model/kimi_k3/components/language_model.py should check for sm capability before selecting marlin kernels.
Kimi K3 in M*. The model, the resources it needs, the kernels, and speculative decoding with the DSpark draft. It serves the expert-pruned
mgoin/Kimi-K3-pruned75checkpoint (224 experts, MXFP4) on one 8xH100 node at TP8. The full checkpoint has the same architecture and needs more nodes.What does this PR do?
Model (
mstar/model/kimi_k3/). A plain reference implementation that the tests check against the HF modeling code, plus the serving version. KDA layers run on FlashKDA and fla kernels with their state in a slot pool (RecurrentStatePool, aKDAvariant ofLinearAttnManager), gated NoPE MLA runs on FlashInfer with absorbed projections, the latent MoE runs on a vendored copy of vLLM's Marlin MXFP4 kernel (JIT built once per machine), and the block attention residuals, norms, SiTU, router and residual adds are fused into a few Triton kernels. Decode runs in CUDA graphs at batch sizes 1 to 64, prefill eagerly in steps of up to 8 prompts.Resources and engine. Merged input projections behind one GEMM, expert parallelism as an
ExpertShardingplacement, symmetric-memory all-reduce on by default for single-node TP (Lamport one-shot for small tensors, NVLink multicast above that, NCCL for large prefill messages), TP async scheduling as the model's default, decode rows riding in prefill steps as an option (mixed_prefill_decode), a torch.profiler window over served steps (MSTAR_TORCH_PROFILE), and a few fixes that came up on the way to TP8 (token counting on follower ranks, SIGTERM in the workers, the shared JIT lock, the loader's device mapping).Speculative decoding.
model_kwargs.speculative_tokensturns each decode step into a verify of k drafted tokens inside the same declare / admit / plan / forward / commit protocol and the same captured graphs. Verification is greedy or sampled (FlashInfer's chain sampling), the KV trims rejected tails, and the KDA state is never rolled back. The pool keeps a checkpoint plus the pending prefix and each step runs prefix and block in one Triton launch.speculative_draftloads theInferact/Kimi-K3-DSparkdraft andspeculative_schedulepicks the block length per capture bucket ({16: 7, 32: 4, 64: 0}for pruned75).Prefill. The KDA prefill path used to sync with the host twice per layer through fla's chunk-table cache. That is gone. The Marlin prefill slice is 8192 tokens, the routed latent is reduce-scattered at prefill sizes with a sharded RMSNorm, and slices of 6144 tokens or more run the experts as bf16 grouped GEMMs on weights dequantized once per layer from the Marlin tiles (
MSTAR_MOE_BF16_TOKENS, warmed at setup).Numbers
pruned75 at TP8 on 8xH100, 1024 tokens in and 512 out, 8C requests per level, output tokens per second. vLLM 0.28 plain with prefix caching off on the same prompts, each system on its own node of the same kind. Measured on the branch before
mainwas merged in.Steady decode (inter-token latency after the first token) is under vLLM's at every level up to C 32, 16.5 vs 17.6 ms at C 1 and 33.8 vs 34.9 at C 32. The C 8 TTFT mean carries one 3 s request that paid the bf16 path's first compile (the p50 is 1039 ms). The warm-up at setup that removes that landed after this sweep and has not been re-swept on the big model. The C 1 and C 2 draft rows have only 8 and 16 requests and the acceptance on this pruned checkpoint moves with the text, so read them as noisy.
The vLLM column is plain vLLM 0.28 without speculation, which is its best configuration on H100. We did measure vLLM with the same DSpark draft and it came out far slower than plain vLLM, 33 to 58 tok/s at C 1 to 16, because vLLM's DSpark needs the FlashInfer MLA backend that is Blackwell-only, falls back to Triton MLA on H100 and loses full CUDA graphs (about 176 ms per step). So the "M* plain" row is the like-for-like comparison, and the draft rows show what speculation adds on our side against the fastest vLLM setup available on this hardware, not against vLLM's speculative path. On Blackwell, where vLLM's DSpark runs at full speed, the fair comparison would be both systems with the draft, and we have not measured that yet.
How to run it
configs/kimi_k3_pruned75_tp8_dspark_sched.yamladds the draft with the block schedule. The tiny random-weight configs (kimi_k3_tiny*.yaml, TP1 and TP2 variants) exercise every path on one or two GPUs.How was it tested?
test/kimi_k3(parity with the HF modeling code on the tiny checkpoint, dense vs paged, TP sharding, quantized experts, tokenizer) and the modular tests for the recurrent pool, the verify path, acceptance, the sampler and the worker.mainis merged in (one conflict, the model registry).Checklist
ruff check .passes