GLM-5.3-Flash¶
models/glm5_3_flash/ is the implementation staging area for
GLM-5.3-Flash (glm5_next,
Glm5NextForConditionalGeneration) on Ascend a2a3. This first milestone
establishes the configuration, the layer schedule, the metadata lowering, the
shared Torch goldens, the W8A8 references, and one @pl.jit.inline ABI per
operator. Kernel bodies are the parallel work that follows.
Why W8A8, and why a2a3 forces it¶
The released checkpoint is FP8 blockwise (e4m3, 128x128, dynamic activations).
The a2a3 cube cannot execute it: Intrinsic_mmad in
CANN's Ascend910_9392.ini SoC config offers only s32s8s8 / u32u8u8 / u8s8 and the
fp16/fp32 forms, with no fp8 or MX entry. INT8 is therefore the only quantized
path on this platform, and the deployment weights are the msmodelslim W8A8
conversion that vLLM Ascend serves on a 16-card A3 node. The int8 cube fractal
is MKN 16, 32, 16, so every quantized K tile must be a multiple of 32.
Checkpoint shape¶
| Property | Value |
|---|---|
| Hidden size | 4,096 |
| Backbone layers | 45 (+1 MTP layer) |
| Linear-attention (KDA) layers | 34 |
| Sparse-attention (NoPE MLA + kpool indexer) layers | 11 (+ the MTP layer) |
| Attention heads | 64 |
MLA q_lora_rank / kv_lora_rank |
1,536 / 512 |
MLA qk_head_dim / v_head_dim / qk_rope_head_dim |
256 / 256 / 0 |
Indexer heads / head dim / index_topk / index_kpool |
32 / 128 / 2,048 / 4 |
| KDA heads / head dim / conv kernel | 64 / 128 / 4 |
| Routed experts / active experts / shared | 288 / 8 / 1 |
| Dense layers / dense intermediate | 3 / 12,288 |
| MoE intermediate | 2,048 |
| Hyper-connection width / Sinkhorn iterations | 4 / 20 |
| Vocabulary | 154,880 |
| Maximum position | 1,048,576 |
The attention schedule is three KDA layers then one sparse layer, repeating:
layers 3, 7, 11, 15, 19, 23, 27, 31, 35, 39 and 43 are sparse, every other
backbone layer is KDA. The first three layers (all KDA) use a dense MLP;
the remaining 42 and the MTP layer are MoE. Checkpoint layer 45 is the MTP
layer: an MLA layer with its own indexer and a sparse MoE, reached through
enorm / hnorm / eh_proj, and it carries no hc_* weights, so it uses a
plain residual rather than the four-stream mHC.
Summing every parameter group from the checkpoint index gives 321.2 G, which matches the published 320 B total, so the reconstruction above is complete.
What differs from DeepSeek-V4, which is the nearest port in this repo¶
Glm5NextTextHyperConnection subclasses DeepseekV4HyperConnection unchanged, so
the mHC coefficient algebra is identical to models/deepseek_v4_1_flash/mhc.py.
Three things are not:
- The hyper-connection stream is BF16. It is seeded by replicating the BF16
embedding (
inputs_embeds.unsqueeze(2).expand(-1, -1, 4, -1)) and every layer casts the mixes into that dtype. DeepSeek-V4 carries the stream in FP32, so the tile dtypes differ throughout and a kernel cannot be lifted across unchanged. At width 4 this is 32 KB of live residual per token instead of 64 KB. - The final collapse is an unweighted mean, not a learned head applying the
last layer's delayed pre-mix, so
mhc_headtakes no mix input. - There is no RoPE anywhere.
qk_rope_head_dimis 0 and the config validator rejects any other value, so the rope/nope split disappears from the main attention — and the indexer has none either, despiteindexer_rope_interleavebeing set inconfig.json. That field is inherited from the GLM-MoE-DSA base and is dead here:Glm5NextTextConfigsetsrope_parameters = AttributeError(),Glm5NextTextModel.forwardpassesposition_embeddings=None, andGlm5NextTextIndexer.forwardnever touches a cos/sin table. So this port needs none of therope_tables.py/ interleaved-rope machinery that bothdeepseek_v4_proanddeepseek_v4_flash_mtpcarry.
Two more differences bite at the kernel level: the router scores with sigmoid
where DeepSeek-V4.1 uses sqrt(softplus(.)), and 288 experts do not fit the
SCORE_PAD = 256 sort window that models/deepseek_v4_flash_mtp/gate.py uses —
the pad has to grow to 512 and the pl.sort32 + pl.mrgsort merge tree gains a
level.
Deployment target¶
One Atlas 800 A3 node, 16 dies, TP16 with expert parallelism over the same 16
ranks, mirroring vLLM Ascend's recipe for this checkpoint
(--tensor-parallel-size 16 --enable-expert-parallel --quantization ascend,
MTP with 3 speculative tokens, FULL_DECODE_ONLY graph capture). Smaller TP/EP
shapes stay available for bring-up through --tp and --ep.
Per rank that gives 4 MLA heads, 4 KDA heads, 18 routed experts and 9,680 vocabulary rows. The indexer is replicated, not head-sharded: its score sums over all 32 heads before the top-k, so sharding would force a cross-rank reduction of partial scores on every sparse layer, and the kernel is small enough that 16-way redundant compute is the cheaper trade. Weights come to about 21.6 GB per rank, of which 19.5 GB is the routed experts; the remaining budget on a 64 GiB die (~61.3 GiB visible) goes to the hybrid cache, which is the binding constraint and is sized by the cache-manager work item.
What is quantized¶
Activations are quantized dynamically per token the way the a2a3 W8A8 sibling
does it: each row's amax, floored by INT8_AMAX_EPS = 1e-4, is rescaled to
INT8_SCALE_MAX = 127, rounded through int32 and fp16 to match device rounding.
The router kernel produces that INT8 view once and both the shared expert and the
EP dispatch payload reuse it.
The boundary below is read from the deployment checkpoint
(Eco-Tech/GLM-5.3-Flash-w8a8 on modelers.cn), not inferred: its
quant_model_description.json labels every tensor except rot.weight, and the
shard headers confirm the dtypes. The split is sharper than the released FP8 checkpoint's.
| Tensor | Format |
|---|---|
mlp.experts.N.{gate,up,down}_proj, mlp.shared_experts.*, and the three dense layers' mlp.{gate,up,down}_proj — the FFN and nothing else |
INT8, with an FP32 [out, 1] per-output-channel weight_scale |
All of attention: every MLA projection (q_a_proj, q_b_proj, kv_a_proj_with_mqa, kv_b_proj, o_proj), every KDA projection and conv, every indexer weight, all mHC tensors, mlp.gate.weight, e_score_correction_bias, every norm, embed_tokens, lm_head, and the whole vision tower |
BF16 |
| MLA 512-latent cache | BF16 |
| Indexer state cache | FP32 today, 256 wide per token; the sibling port quantizes its indexer cache to INT8 with a per-row FP32 scale, which would save about 2.3 KB per token here |
| Hyper-connection stream | BF16 — see above, this is where GLM departs from DeepSeek-V4's FP32 stream |
| Logits | FP32 |
Of 113,352 labelled tensors, 111,870 are W8A8_DYNAMIC and 1,482 are FLOAT. Each
quantized weight also carries a weight_offset, but it is all zero — measured
across tensors of 12,288, 2,048 and 4,096 channels — so the weight quantization is
symmetric and the loader reads and discards the offset. There is no static
activation path and no SmoothQuant vector.
The consequence for the work list is that W8A8 is confined to the MoE and dense-MLP
streams. Attention carries no quantization at all, which removes the scale plumbing
from mla_prolog, mla_epilog and the indexer projections.
The checkpoint also ships two matrices that are not weights¶
rot.weight, BF16 [4096, 4096], is the only tensor in the weight index with no
entry in the quantization description; it is announced solely by is_rot_used:
true. It belongs to the MTP layer, not the backbone — vLLM Ascend's
AscendDeepSeekMTP applies it to the previous hidden state before hnorm. GLM
routes to Glm5NextMTP instead, which has no rot and drops the tensor silently,
so whether it is required is an open question the MTP owner inherits. It is not a
rotation, despite the name: it is symmetric with R[i,j] = g(i xor j), a
per-channel diagonal scaling in the Hadamard basis.
optional/quarot.safetensors holds global_rotation, F32 [4096, 4096], an exact
scaled Hadamard (every entry +/-1/64, rows orthonormal) — a textbook QuaRot matrix.
It is unreachable for this checkpoint: vLLM Ascend looks it up under
quant_description["optional"]["quarot"], and this description's optional is
empty. Where that matrix is used elsewhere in vLLM Ascend it is folded into weights
offline, never applied to activations. No backbone kernel applies a rotation.
The one rotation that does matter is 128 wide and lives in the indexer: the upstream kernel rotates each query head by a Hadamard-128 before quantizing it. See the indexer files.
Prior art, and what it is actually worth¶
None of the references below is drop-in code for this repo, but two of them are worked designs for the hardest kernels on our exact platform.
| Reference | Where | What it gives us |
|---|---|---|
| Chunked KDA in pypto, tuned on Ascend910B3 | cann-recipes-infer, integration/vllm/ling-3.0-flash/npu_patch/.../ops/pypto/kda/chunk_kda_impl.py |
A complete chunked delta-rule forward at T=4K, H=4, K=V=128 — our per-rank TP16 shape — with a design write-up covering the M1-M4 stages, the 8x8-leaf block inverse, and the two bottlenecks whose fix was worth 3.8x |
| Recurrent KDA in pypto | its fused_recurrent_kda_impl.py sibling |
The decode-step counterpart |
| Lightning indexer, MLA prolog, hc_pre, sparse attention in pypto | cann-recipes-infer, ops/pypto_python/impl/ |
Algorithm and tiling references for the indexer and MLA items |
vLLM Ascend glm5next |
vllm_ascend/models/glm5next/ |
The only NPU port of this architecture: module decomposition, cache specs, W8A8 wiring, MTP. It does not implement the kpool indexer — sparse_attn_indexer_kpool.py raises and says the upstream is CUDA-only |
| CANN recipes GLM-5 / GLM-5.2 | cann-recipes-infer, models/glm_5{,_2}/ |
Ascend-native MLA + DSA + MoE with a 16-rank W8A8 A3 config. No KDA and no mHC |
| GLM-4.5 pypto operators | the open-source pypto mirror, models/glm_v4_5/ |
MoE gate, expert selection and the shared-expert W8A8 patterns |
The two pypto references above target a different pypto frontend
(pypto.frontend.jit / pypto.Tensor / pypto_impl / auto-tiling). The pypto
this repo pins exports only jit, language, ir, runtime and friends, so
those files are design references, not code to copy: the port is from implicit
tiling to the explicit pl tile DSL.
Platform envelope¶
a2a3 (Ascend910_9392) |
Value | a5 for contrast |
|---|---|---|
| AIC / AIV per die | 24 / 48 | 36 / 72 |
PLATFORM_MAX_BLOCKDIM / max cores |
24 / 72 | 36 / 108 |
| UB (safe) | 192 KB (~184 KB) | 248 KB |
| L1 / L0A / L0B / L0C | 512 / 64 / 64 / 128 KB | — |
| HBM per die | 64 GiB (~61.3 GiB visible) | — |
fp8 / MX mmad |
none | supported |
| 4-bit in-core dtypes | not supported | FP4 supported |
Acc -> Vec and Vec -> Mat memory edges |
absent | present |
| AIC/AIV cross-core pipe | through GM | on-chip |
| GM access granularity | 512 B | 128 B |
| bf16 atomic add into GM | supported | not supported |
Some of these change how a kernel is written rather than how fast it runs. The
missing Acc -> Vec edge forces a GM round trip where a5 would fuse. INT8 can only
reach GM through a Vec tile or the quantized fix-pipe path. And not in the table but
worth knowing before the first MIX kernel: a2a3 requires dual-AIV dispatch even for
a no-split MIX kernel, or the AIC cross-core handshake deadlocks.
What stream B's a2a3sim bring-up surfaced¶
Each of these was a compile or simulator failure, not a guess:
- The simulator needs a real GCC 15. A
g++-15shim pointing at GCC 13 builds simulator kernels that get every BF16 matmul wrong while FP32 still passes, so the failure looks like a kernel bug.deepseek_v4_flash_mtp/decode_compressor_ratio4.pyis a quick canary: it passes in CI and fails under such a shim. - Do not mix the Tensor and Tile levels.
pl.slice, bracket slices,pl.full,pl.matmul_accand slice assignment are Tensor level;pl.load,pl.tile.full,pl.create_tileandpl.storeare Tile level. A Tensor-level result is written withdest = pl.assemble(dest, pl.set_validshape(src, rows, cols), offset), a Tile-level one withpl.store;pl.writeworks on a Tensor-level on-core buffer wherepl.tile.writedoes not. tmp_tileis Tile-only.pl.row_sum/pl.row_maxon a Tile must take one; on a Tensor they must not. High-precisionrsqrton a Tile ispl.tile.rsqrt(x, tmp), nothigh_precision=True.- FP32 reductions need a 32-byte column, and a row-major tile needs a 32-byte row,
so an
[H, 1]column wantsH % 8 == 0and is best built as[1, H]then reshaped. - A matmul is only split into L0 tiles when M reaches 16. Below that the 128 x 512 KV operand of the sparse attention stays whole and overflows the 64 KB right buffer; the TP16 attention pads its 4 heads to 16 zero-filled rows for this.
- No mutable scalar flags across branches. A Python int set in one
ifand read in another becomes a phi the backend cannot materialise; write each branch's work in place, as the donors do. Loop-carried tile state goes throughpl.yield_on every path. - In a mixed cube/vector scope, no branch may decide whether loop-carried state is
updated. When the carried tiles flow through an
if/else, the partitioner materialises their pre-looppl.fullinitialisation in the cube half as well, andccecrejectsvector_dup/set_vector_maskfordav-c220-cube.a2a3simcompiles both halves withg++and never sees it, so this reaches CI as an a2a3-only failure (run 35555578466). Let the branch pick the block's inputs and keep the merge unconditional, or split the scope by hand withpl.split_aic/pl.split_aivasdeepseek_v4_flash_dspark/prefill_sparse_attn.pydoes. The check needs no card: compile with-p a2a3 --compile-onlyand assert that the generatedkernels/aic/*.cppcarries no vector op between#if defined(__DAV_CUBE__)and its#endif(TMULthere is address arithmetic and is expected). pl.spmdtakes a Var, not a call. Bindpl.tensor.dim(...)to a name first.- A script-entry file must put the repository root first on
sys.path, orfrom golden import runresolves to this directory's owngolden.py.
How this directory is organised for parallel work¶
Every operator has one ownership file. Each file carries the exact maths in its
module docstring, a golden_* Torch reference, and a typed @pl.jit.inline ABI
whose body raises NotImplementedError until its owner lands it. Shapes come
from config.py, so two engineers working on neighbouring operators agree on the
interface without talking to each other.
A file has no script-entry guard until its golden is real. The a2a3 daily CI
selects a case purely by grepping for that guard, so adding one to a file whose
golden still raises would put an unimplementable operator into the sweep. Today
config.py, golden.py, quantization.py, metadata.py, mhc.py and all five
stream-B MLA files carry real references, and eight of them run as CI cases:
source .venv/bin/activate-pypto
python models/glm5_3_flash/golden.py # [GOLDEN] PASS norms / swiglu / moe_gate
python models/glm5_3_flash/quantization.py # [GOLDEN] PASS quantization
python models/glm5_3_flash/mhc.py # [GOLDEN] PASS mhc
python models/glm5_3_flash/mla_prolog.py # [GOLDEN] PASS mla_prolog + prolog/absorb
python models/glm5_3_flash/mla_cache.py # [GOLDEN] PASS mla_cache + device scatter
python models/glm5_3_flash/mla_epilog.py # [GOLDEN] PASS mla_epilog + both paths
python models/glm5_3_flash/prefill_sparse_attn.py # [GOLDEN] PASS + device sparse prefill
python models/glm5_3_flash/decode_sparse_attn.py # [GOLDEN] PASS + device sparse decode
models/deepseek_v4_1_flash/mhc.py is the worked example of a finished item: a
golden, the four kernel bodies, a @pl.jit test entry, build_mhc_tensor_specs
and a precision comparison, in 559 lines. Read it for the shape, not for the
kernels — it is tagged # ci: a5 and # ci: no-sim, and it even splits the
spelling of the entry sentinel (_SCRIPT_ENTRY_POINT = "__" + "main__") so the
a2a3 sweep cannot pick it up. That idiom is the escape hatch if this directory ever
needs a file that is runnable by hand but must stay out of the a2a3 job.
For kernels you actually intend to run here, prefer the untagged a2a3 files:
models/deepseek_v4_flash_mtp/ has implemented hc_pre.py, hc_post.py and
hc_head.py, gate.py, expert_routed.py, expert_shared.py, decode_indexer.py,
prefill_indexer.py and decode_sparse_attn_csa.py, all of which the a2a3 daily
sweep runs today.
Five streams, five owners¶
The work divides into five self-contained streams. Each owns a disjoint set of
files, so two owners never edit the same file, and they meet only at the ABIs that
config.py and the @pl.jit.inline signatures already fix. Stream E owns those
contracts and publishes them first.
| Stream | Owns | Files | Items | Days | Long pole |
|---|---|---|---|---|---|
| A | KDA linear attention — 34 of the 45 backbone layers | 5 | 10 | 50-82 | kda_chunk_delta_prefill — the chunked delta rule, 20-32 days on its own |
| B | NoPE MLA sparse attention — 11 sparse layers + the MTP layer | 5 | 6 | 45-72 | mla_sparse_attn_prefill — gather-and-attend over 2051 selected rows, 15-25 days |
| C | kpool DSA indexer and mHC — 12 indexer instances; mHC on all 45 layers | 4 | 11 | 38-60 | indexer_cache — it blocks five downstream items, so land it in week one |
| D | MoE, primitives, output head, MTP — 43 sparse layers, 3 dense layers, the draft step | 12 | 14 | 61-100 | moe_dispatch_ep16 — the 16-rank all-to-all, 14-22 days |
| E | Runtime, weight loading, composition — the whole model | 9 | 6 | 76-120 | layer_composition — 40-60 days, and by definition it lands last |
| (staged) | Vision tower — prefill of multimodal requests only | 5 | 6 | 23-35 | vision_attention — non-causal varlen attention, no donor in this repo |
Phase one is streams A-E: 269-434 engineer-days across five owners, before any tuning. The vision tower is staged out — the A3 recipe serves this checkpoint text-only unless --limit-mm-per-prompt is set, and the tower is 0.53 G parameters against the text model's 320 G.
Stream A — KDA linear attention¶
34 of the 45 backbone layers · 5 files · 10 work items · 50-82 engineer-days
Files. kda_projection.py, kda_conv.py, prefill_kda.py, decode_kda.py, kda_output.py
Where to start. No donor in pypto-lib. The pl-adjacent reference is the tuned Ascend910B3 chunk_kda_impl.py / fused_recurrent_kda_impl.py in cann-recipes, plus its design doc.
| Work item | Verdict | Size | Days | Depends on |
|---|---|---|---|---|
kda_chunk_delta_prefill |
SPLIT | XL | 20-32 | kda_qkv_proj |
kda_recurrent_decode |
ADAPT | L | 8-13 | — |
kda_conv1d_prefill |
NEW | M | 4-7 | kda_qkv_proj, kda_conv1d_decode |
kda_out_proj |
ADAPT | M | 4-7 | mhc_post |
kda_conv1d_decode |
NEW | M | 3-5 | kda_state_cache, kda_qkv_proj |
kda_state_cache |
NEW | M | 3-5 | — |
kda_gates |
NEW | S | 2-4 | kda_qkv_proj |
kda_qkv_proj |
ADAPT | S | 2-3 | — |
kda_l2norm_qk |
ADAPT | S | 1.5-3 | — |
kda_out_gated_norm |
ADAPT | S | 2-3 | — |
Stream B — NoPE MLA sparse attention¶
11 sparse layers + the MTP layer · 5 files · 6 work items · 45-72 engineer-days
Files. mla_prolog.py, mla_cache.py, prefill_sparse_attn.py, decode_sparse_attn.py, mla_epilog.py
Where to start. deepseek_v4_flash_mtp/qkv_proj_rope.py, prefill_sparse_attn.py and decode_sparse_attn_csa.py. Same 512-wide KV row; delete the sliding window, the ratio-4 slot rewrite, the attention sink and all of the RoPE.
| Work item | Verdict | Size | Days | Depends on |
|---|---|---|---|---|
mla_sparse_attn_prefill |
ADAPT | XL | 15-25 | — |
mla_sparse_attn_decode |
ADAPT | L | 9-14 | — |
mla_epilog_o_proj |
SPLIT | L | 8-13 | — |
mla_prolog_nope |
ADAPT | M | 6-9 | — |
mla_kv_absorb |
ADAPT | M | 4-6 | — |
mla_latent_cache |
ADAPT | S | 3-5 | — |
Stream C — kpool DSA indexer and mHC¶
12 indexer instances; mHC on all 45 layers · 4 files · 11 work items · 38-60 engineer-days
Files. prefill_indexer.py, decode_indexer.py, indexer_cache.py, mhc.py
Where to start. deepseek_v4_flash_mtp/{prefill,decode}_indexer.py for the selection pipeline (its _cp_topk512_query is the same K over the same candidate cap), and hc_{pre,post,head}.py for mHC. The k-pooling stage has no donor anywhere.
| Work item | Verdict | Size | Days | Depends on |
|---|---|---|---|---|
indexer_cache |
SPLIT | L | 7-10 | — |
indexer_score |
ADAPT | L | 6-9 | indexer_cache |
indexer_kpool_compress |
ADAPT | M | 5-8 | indexer_cache |
indexer_topk512 |
ADAPT | M | 4-6 | indexer_score, indexer_cache |
indexer_qk_proj |
NEW | M | 3-5 | — |
indexer_share_mtp |
NEW | M | 3-5 | indexer_score, indexer_cache |
mhc_pre |
ADAPT | M | 3-5 | mhc_post |
mhc_mixes |
ADAPT | S | 2-4 | — |
indexer_expand_tail |
NEW | S | 2-3 | — |
mhc_post |
ADAPT | S | 1.5-3 | mhc_pre, mhc_mixes |
mhc_head |
ADAPT | S | 1-2 | mhc_post |
Stream D — MoE, primitives, output head, MTP¶
43 sparse layers, 3 dense layers, the draft step · 12 files · 14 work items · 61-100 engineer-days
Files. gate.py, expert_routed.py, expert_shared.py, dense_mlp.py, (new) prefill_moe.py, (new) decode_moe.py, rmsnorm.py, quantization.py, lookup_embedding.py, lm_head.py, mtp_projection.py, (new) decode_mtp.py
Where to start. Almost every file has a same-named donor in deepseek_v4_flash_mtp that the a2a3 sweep already runs. This is the most port-like stream in the model.
| Work item | Verdict | Size | Days | Depends on |
|---|---|---|---|---|
moe_dispatch_ep16 |
SPLIT | XL | 14-22 | moe_gate |
mtp_draft_loop |
SPLIT | L | 12-18 | indexer_score, indexer_cache |
moe_expert_routed_w8a8 |
ADAPT | M | 5-8 | moe_dispatch_ep16, moe_gate, moe_combine_ep16 |
moe_combine_ep16 |
ADAPT | L | 5-8 | moe_dispatch_ep16, moe_gate, moe_expert_routed_w8a8 |
lm_head_tp16 |
ADAPT | M | 4-7 | mhc_head |
dense_mlp_w8a8 |
ADAPT | M | 4-6 | moe_shared_expert_w8a8 |
moe_gate |
ADAPT | M | 3-5 | — |
moe_shared_expert_w8a8 |
ADAPT | S | 3-5 | moe_gate, w8a8_weight_loader |
embed_tokens |
ADAPT | M | 3-5 | — |
swiglu_clamp_quant |
ADAPT | S | 2-4 | — |
mtp_eh_proj |
ADAPT | S | 2-4 | rms_norm |
w8a8_linear |
SPLIT | S | 1.5-3 | — |
rms_norm |
ADAPT | S | 1.5-2.5 | — |
quant_per_token_int8 |
ADAPT | S | 1-2 | moe_gate |
Stream E — Runtime, weight loading, composition¶
the whole model · 9 files · 6 work items · 76-120 engineer-days
Files. config.py, metadata.py, (new) weights.py, (new) cache_manager.py, attention_tp.py, (new) prefill_layer.py, (new) decode_layer.py, (new) prefill_fwd.py, (new) decode_fwd.py
Where to start. deepseek_v4_flash_mtp/{prefill,decode}_{layer,fwd}.py, about 8,600 lines of donor. The 3:1 KDA/DSA schedule replaces its SWA/CSA/HCA schedule.
| Work item | Verdict | Size | Days | Depends on |
|---|---|---|---|---|
layer_composition |
SPLIT | XL | 40-60 | mhc_head, mhc_post, mhc_mixes |
tp16_collectives |
SPLIT | L | 15-27 | forward_metadata |
w8a8_weight_loader |
SPLIT | L | 8-12 | indexer_score |
hybrid_kv_cache |
ADAPT | L | 6-9 | indexer_cache |
forward_metadata |
ADAPT | M | 5-8 | — |
graph_capture |
ADAPT | S | 2-4 | — |
Stream V — Vision tower (staged)¶
prefill of multimodal requests only · 5 files · 6 work items · 23-35 engineer-days
Files. vision/patch_embed.py, vision/attention.py, vision/mlp.py, vision/merger.py, vision/fusion.py
Where to start. No donor in this repo.
| Work item | Verdict | Size | Days | Depends on |
|---|---|---|---|---|
vision_attention |
NEW | L | 8-12 | vision_patch_embed |
vision_mlp |
SPLIT | M | 5-7 | vision_patch_embed, vision_attention |
vision_rope_qkv_norm |
ADAPT | M | 4-6 | vision_patch_embed, vision_attention |
vision_merger |
NEW | M | 3-5 | vision_patch_embed, vision_attention, vision_mlp |
mm_fusion |
NEW | S | 2-3 | vision_merger |
vision_patch_embed |
ADAPT | S | 1-2 | — |
Sequencing¶
Three items block the most downstream work and should go out in week one:
indexer_cache (blocks five), moe_gate (blocks five) and kda_qkv_proj (blocks
four). Stream E's contract work — forward_metadata, w8a8_weight_loader,
tp16_collectives, hybrid_kv_cache — is the other week-one batch, because
streams A through D cannot run anything on device without it.
That gives stream E a bookend shape: about 36-60 days of shared contracts up front,
then layer_composition at the end. It is the integration role, and it is also the
natural reviewer for the other four.
Where each file's donor lives¶
models/deepseek_v4_flash_mtp/ is the a2a3 W8A8 sibling, and almost every GLM file
has a same-shaped counterpart there that the a2a3 daily sweep already runs. The
port is, in one sentence: take that model, delete the compressor / sliding-window /
RoPE machinery, and add the KDA family and the kpool indexer.
| GLM file | a2a3 donor | What changes |
|---|---|---|
mhc.py |
hc_pre.py, hc_post.py, hc_head.py |
FP32 stream becomes BF16; the learned head becomes an unweighted mean |
rmsnorm.py |
rmsnorm.py |
Add the INT8-emitting variant for the three dense layers |
gate.py |
gate.py |
sqrt(softplus(.)) becomes sigmoid; SCORE_PAD 256 to 512; drop hash routing and the group mask |
expert_routed.py, expert_shared.py |
same names | 288/8 instead of 256/6, moe_intermediate_size 2048 |
lookup_embedding.py, lm_head.py |
same names | Vocabulary 154,880; the embedding also seeds the four HC streams |
mtp_projection.py |
mtp_projection.py |
Same enorm / hnorm / eh_proj shape |
mla_prolog.py |
qkv_proj_rope.py |
Delete the whole RoPE half; add the kv_b_proj absorption |
prefill_sparse_attn.py, decode_sparse_attn.py |
prefill_sparse_attn.py, decode_sparse_attn_csa.py |
Delete the sliding-window gather, the ratio-4 slot rewrite, the attention sink and the inverse RoPE |
prefill_indexer.py, decode_indexer.py |
same names | Delete the INT8/Hadamard half and the compressor; add the k-pooling stage |
(new) prefill_moe.py, decode_moe.py |
same names | EP16 instead of EP8; 18 local experts |
(new) prefill_fwd.py, decode_fwd.py |
same names | The 3:1 KDA/DSA schedule replaces the SWA/CSA/HCA schedule |
kda_*.py, the k-pooling stage |
no donor in this repo | See the prior-art table above |
What already exists in the directory¶
| File | State |
|---|---|
config.py |
Complete. Layer schedule, derived per-rank shapes, TP/EP validation, W8A8 metadata |
golden.py |
Complete for the shared transforms: RMSNorm, gated RMSNorm, L2 norm, clamped SwiGLU, the expert body, the sigmoid noaux_tc router, and the four mHC references |
quantization.py |
Complete. Per-channel and per-token INT8 with the repo's exact rounding, the dynamic W8A8 linear, and the fused dequant-SwiGLU-requant epilogue |
metadata.py |
Complete. Packed-batch lowering, the three distinct slot mappings (latent, per-token indexer state, pooled state), TP token ownership, and the indexer's pool and tail derivation |
_golden_smoke.py |
Complete. Deterministic CPU fixtures behind every golden that exists |
mla_prolog.py |
Goldens, the prolog body (three b_trans matmuls and two rms-norm scopes, no split-K yet), the per-token absorb_query body, and a device test entry per case. absorb_output is weight-time and belongs to the loader. Passes a2a3sim and a2a3 |
mla_cache.py |
Golden, kernel body (pl.spmd scatter, one block per row), device test entry and specs. Passes a2a3sim and a2a3 |
mla_epilog.py |
Goldens, both kernel bodies (row-parallel b_trans projection over D, FP32 partial sum for the TP16 all-reduce) and a device test entry per path. Passes a2a3sim and a2a3 |
prefill_sparse_attn.py |
Golden, kernel body (one item per token, pl.yield_-carried online softmax, value expansion after the block loop) and a device test entry. Attends in latent space: absorbing keeps the two expansions out of the per-token block loop, at the cost of two BF16 roundings against the expanded reference. An all-padding block is filled by one wide gather and merged with beta = 0 rather than skipped, so no branch carries the online-softmax state. Requires the front-packed topk_indices ABI. Passes a2a3sim and a2a3 |
decode_sparse_attn.py |
Golden, kernel body (24 lanes over (token, sparse block), L1 gather, flash partials, alpha/beta merge, empty-block skip) and a device test entry. Requires the front-packed topk_indices ABI. Passes a2a3sim and a2a3 |
attention_tp.py |
ABI only, but it is the one shared consumer: kda_output, both mla_epilog entries and dense_mlp all emit an FP32 row-parallel partial, and this is what adds the 16 of them |
| everything else | ABI and docstring only; the golden and the kernel body are the assignment |
Open questions an owner will hit¶
- Whether the MTP layer owes
rot.weight.is_rot_used: trueand the tensor's presence in the index argue yes;Glm5NextMTPdropping it argues no, or argues the reference port has a gap. Stream D settles it before implementing MTP. - How the indexer cache is laid out. vLLM Ascend splits it into an index-K cache and a separate FP32 pooled-state page class with a page of four tokens; storing the raw row instead is simpler but larger. Stream C decides, stream E's cache manager implements.
- Whether the MLA decode path absorbs.
kv_b_projis BF16 in the checkpoint, so folding it into an INT8o_projchanges the numerics of the epilogue. - The cache budget. At about 18 KB per token per rank the hybrid cache holds
roughly 1.5 M tokens per node once weights are placed, so the recipe's
--max-model-len 133120and--max-num-seqs 32cannot both be satisfied at full length.