metrale_model_arch/glm5next_mtp_head/init.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `Glm5NextMtpHead::new`: the drafter's private KV pool and its vocab-sharded draft head.
4//!
5//! Owner: model-arch (GLM-5.3 MTP drafter).
6//! Invariants:
7//! - A constructed head's `max_seq_len` is at most `max_dsa_context` of its DSA block.
8//! - `head_n` is `vocab_size / tp_world_size` when `tp_world_size > 1` divides
9//! `vocab_size`, and `vocab_size` otherwise.
10
11use super::*;
12
13impl Glm5NextMtpHead {
14 pub fn new(
15 module: Glm5NextMtpModule,
16 embed_tokens: DenseWeight,
17 lm_head: DenseWeight,
18 config: &metrale_config::ModelConfig,
19 gpu: &dyn GpuBackend,
20 max_seq_len: usize,
21 ) -> Result<Self> {
22 let dsa = match &module.layer.mixer {
23 crate::glm5next_layer::Glm5NextMixer::Dsa(l) => l,
24 _ => bail!("GLM MTP block is not a DSA layer"),
25 };
26 // 2026-09-25: The drafter block is a DSA layer, so it can never reach a position past
27 // `max_dsa_context`: that is the indexer cache `Glm5NextDsaState::alloc` reserves, and
28 // `advance` refuses to grow past it. Everything sized from `max_seq_len` here uses the
29 // capped value: the private KV pool below, the block table claimed in `alloc_state`,
30 // the bounds checks in `forward_one` and `rows_impl`, and, through
31 // `prefill_hidden_rows`, the model's `mtp_prefill_hidden` capture.
32 let max_seq_len = drafter_context_rows(max_seq_len, &dsa.cfg);
33 // 2026-09-25: One KV head of `kv_lora_rank` per token, the absorbed-MLA latent that
34 // the block's `latent_write` and paged gather address.
35 let kv_config = KvCacheConfig {
36 block_size: 16,
37 num_kv_heads: 1,
38 head_dim: dsa.cfg.kv_lora_rank,
39 num_layers: 1,
40 dtype: KvCacheDtype::Fp8,
41 layer_dtypes: vec![],
42 layer_dims: vec![],
43 cache_blocks_per_seq: None,
44 };
45 let blocks = max_seq_len / kv_config.block_size + 2;
46 let kv_cache = PagedKvCache::new(kv_config, blocks, gpu)?;
47 // 2026-09-25: The draft `lm_head` sweep is most of the drafter's time. Measured
48 // 2026-08-29 with `METRALE_GLM_MTP_SKIP=head`: propose 8.84 ms -> 1.52 ms. It reads
49 // `[vocab, hidden]` BF16, 1.27 GB at GLM-5.3's 154,880 x 4,096.
50 //
51 // So the sweep is split by vocab across the ranks that run propose together: each
52 // rank sweeps its `head_n` rows over the full hidden dimension, and the ranks exchange
53 // (max, argmax) through one 16-byte all-reduce (`forward_one`).
54 //
55 // Vocab, not hidden: rows of `[vocab, hidden]` are contiguous, so a vocab shard is a
56 // base-pointer offset and a smaller `n`. A hidden shard would need a row stride, and
57 // the gemv takes none; it assumes rows packed at K.
58 let head_world = config.tp_world_size.max(1);
59 let head_rank = config.tp_rank;
60 let head_n = if head_world > 1 && config.vocab_size.is_multiple_of(head_world) {
61 config.vocab_size / head_world
62 } else {
63 config.vocab_size
64 };
65 // 2026-09-25: Quantise only the rows this rank sweeps: `head_n * hidden` bytes, not
66 // the whole vocab. A failure here is not fatal; the head falls back to the BF16 sweep.
67 let gemv_fp8w_k =
68 metrale_model_layers::layers::try_kernel(gpu, "gemv_fp8w", "dense_gemv_fp8w");
69 let head_fp8 = if std::env::var("METRALE_GLM_MTP_HEAD_FP8").as_deref() == Ok("0")
70 || gemv_fp8w_k.0 == 0
71 {
72 None
73 } else {
74 let shard = DenseWeight {
75 weight: lm_head
76 .weight
77 .offset(head_rank * head_n * config.hidden_size * 2),
78 };
79 match gpu
80 .kernel("gemv_fp8w", "quantize_bf16_to_fp8")
81 .and_then(|qk| {
82 metrale_model_layers::weight_map::quantize_to_fp8(
83 &shard,
84 head_n,
85 config.hidden_size,
86 gpu,
87 qk,
88 gpu.default_stream(),
89 )
90 }) {
91 Ok(q) => {
92 tracing::info!(
93 "GLM MTP: draft lm_head shard quantised to FP8 ({} rows x {}, {} MB)",
94 head_n,
95 config.hidden_size,
96 head_n * config.hidden_size / (1024 * 1024),
97 );
98 Some(q)
99 }
100 Err(e) => {
101 tracing::warn!("GLM MTP: FP8 draft head unavailable ({e:#}); staying BF16");
102 None
103 }
104 }
105 };
106
107 Ok(Self {
108 module,
109 embed_tokens,
110 lm_head,
111 kv_cache: Mutex::new(kv_cache),
112 rms_norm_k: gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?,
113 gemv_k: gpu.kernel("gemv", "dense_gemv_bf16")?,
114 argmax_k: gpu.kernel("argmax", "argmax_bf16")?,
115 hidden: config.hidden_size,
116 vocab: config.vocab_size,
117 max_seq_len,
118 head_rank,
119 head_v0: head_rank * head_n,
120 head_n,
121 head_fp8,
122 gemv_fp8w_k,
123 })
124 }
125}