metrale_model_arch/tp_shard/
gdn.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Tensor-parallel (head-parallel) sharding of the Gated-DeltaNet
4//! (SSM / linear-attention) layers.
5//!
6//! Owner: model-arch (tensor parallelism).
7//! Invariants:
8//! - A segmented slice keeps the segment order and packs each segment's local
9//!   rows back to back; every segment must divide by `tp_size`, else nothing
10//!   is allocated and an error is returned.
11
12use anyhow::{Result, ensure};
13use metrale_config::ModelConfig;
14use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
15
16use super::{BF16_BYTES, TpShardKind, shard_dense_bf16};
17
18// 2026-09-25: Each TP rank owns a contiguous range of key and value heads and
19// runs the recurrence on its local heads; the layer all-reduces after
20// `out_proj` (`ssm_tp_all_reduce` in model-layers' `qwen3_ssm`).
21//
22// The in-projection is segmented, not one flat matrix:
23//
24//   in_proj_qkv : [Q | K | V]        rows = nk·kd + nk·kd + nv·vd  (= conv_dim)
25//   in_proj_z   : [Z]                rows = nv·vd
26//   → gpu_concat_rows → QKVZ         [Q | K | V | Z]
27//
28// A plain "first out_dim/tp rows" slice would give rank 0 the whole Q block
29// plus part of K, so each segment is sliced by the local head range on its own
30// and the local slices are packed in the same order (`segment_copy_plan`).
31//
32// The depthwise `conv1d` weight `[conv_dim, d_conv]` uses the same segments as
33// QKV (one filter per QKV channel). `a_log` / `dt_bias` (`[nv]` FP32) and
34// `out_proj` (`[h, nv·vd]`, row-parallel) shard on the value-head axis. The
35// Qwen3.5 loaders keep the gated-norm weight `norm` whole on every rank. The
36// BA gate buffer is interleaved per key-head group, and a rank's rows start on
37// a group boundary, so it slices as one contiguous block.
38
39/// 2026-09-25: Pre-TP-shard GDN (linear-attention / SSM) dimensions
40/// reconstructed from `config`.
41///
42/// As for [`super::TpAttentionDims`]: `topology.rs` divides
43/// `linear_num_key_heads` / `linear_num_value_heads` by the TP size before any
44/// loader runs, so `config` holds per-rank head counts. The `full_*` fields
45/// multiply back up to the pre-shard sizes the segment slicers take. Head dims
46/// (`kd`, `vd`) and the hidden size `h` are not divided.
47#[derive(Debug, Clone, Copy)]
48pub struct TpGdnDims {
49    pub tp_rank: usize,
50    /// 2026-09-25: `tp_world_size` clamped to `>= 1`.
51    pub tp_size: usize,
52    /// 2026-09-25: Hidden size, not divided per rank.
53    pub h: usize,
54    /// 2026-09-25: Key head dim (`linear_key_head_dim`).
55    pub kd: usize,
56    /// 2026-09-25: Value head dim (`linear_value_head_dim`).
57    pub vd: usize,
58    /// 2026-09-25: Per-rank key heads (Q and K share this count).
59    pub local_nk: usize,
60    /// 2026-09-25: Full pre-shard key heads = `local_nk * tp_size`.
61    pub full_nk: usize,
62    /// 2026-09-25: Per-rank value heads.
63    pub local_nv: usize,
64    /// 2026-09-25: Full pre-shard value heads = `local_nv * tp_size`.
65    pub full_nv: usize,
66}
67
68impl TpGdnDims {
69    pub fn from_config(config: &ModelConfig) -> Self {
70        let tp_size = config.tp_world_size.max(1);
71        let local_nk = config.linear_num_key_heads;
72        let local_nv = config.linear_num_value_heads;
73        Self {
74            tp_rank: config.tp_rank,
75            tp_size,
76            h: config.hidden_size,
77            kd: config.linear_key_head_dim,
78            vd: config.linear_value_head_dim,
79            local_nk,
80            full_nk: local_nk * tp_size,
81            local_nv,
82            full_nv: local_nv * tp_size,
83        }
84    }
85
86    /// 2026-09-25: Full (pre-shard) key projection width: `full_nk * kd`.
87    pub fn full_key_dim(&self) -> usize {
88        self.full_nk * self.kd
89    }
90    /// 2026-09-25: Local key projection width: `local_nk * kd`.
91    pub fn local_key_dim(&self) -> usize {
92        self.local_nk * self.kd
93    }
94    /// 2026-09-25: Full (pre-shard) value projection width: `full_nv * vd`.
95    pub fn full_value_dim(&self) -> usize {
96        self.full_nv * self.vd
97    }
98    /// 2026-09-25: Local value projection width: `local_nv * vd`.
99    pub fn local_value_dim(&self) -> usize {
100        self.local_nv * self.vd
101    }
102    /// 2026-09-25: Full conv / QKV width: `2*full_nk*kd + full_nv*vd`.
103    pub fn full_conv_dim(&self) -> usize {
104        2 * self.full_key_dim() + self.full_value_dim()
105    }
106    /// 2026-09-25: Local conv / QKV width: `2*local_nk*kd + local_nv*vd`.
107    pub fn local_conv_dim(&self) -> usize {
108        2 * self.local_key_dim() + self.local_value_dim()
109    }
110    /// 2026-09-25: Full QKVZ out dim: `2*full_nk*kd + 2*full_nv*vd`.
111    pub fn full_qkvz_out(&self) -> usize {
112        self.full_conv_dim() + self.full_value_dim()
113    }
114    /// 2026-09-25: Local QKVZ out dim: `2*local_nk*kd + 2*local_nv*vd`.
115    pub fn local_qkvz_out(&self) -> usize {
116        self.local_conv_dim() + self.local_value_dim()
117    }
118
119    /// 2026-09-25: Full-row segment list for the `[Q|K|V]` in-projection.
120    pub(crate) fn qkv_segments(&self) -> [usize; 3] {
121        [
122            self.full_key_dim(),
123            self.full_key_dim(),
124            self.full_value_dim(),
125        ]
126    }
127    /// 2026-09-25: Full-row segment list for the concatenated `[Q|K|V|Z]` in-projection.
128    pub(crate) fn qkvz_segments(&self) -> [usize; 4] {
129        [
130            self.full_key_dim(),
131            self.full_key_dim(),
132            self.full_value_dim(),
133            self.full_value_dim(),
134        ]
135    }
136}
137
138/// 2026-09-25: A single device-to-device copy in a segmented-slice plan, in bytes.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub(crate) struct CopyOp {
141    pub(crate) src_off: usize,
142    pub(crate) dst_off: usize,
143    pub(crate) len: usize,
144}
145
146/// 2026-09-25: Build the copy plan for a segmented row-slice.
147///
148/// `segments` lists the full (pre-shard) row count of each contiguous block
149/// (Q, K, V[, Z] for QKVZ). Each block is sliced on its own to the local
150/// rank's range `[tp_rank * seg/tp, (tp_rank+1) * seg/tp)` and the local
151/// slices are packed back-to-back into the output buffer, preserving segment
152/// order. `row_bytes` is the byte width of one row (`in_dim * elem_bytes`).
153///
154/// Returns `(ops, local_total_rows)`. Errors when `tp_rank >= tp_size` or a
155/// segment is not divisible by `tp_size`. `TpGdnDims` segments are multiples of
156/// `tp_size` by construction (`full_* = local_* * tp_size`).
157pub(crate) fn segment_copy_plan(
158    segments: &[usize],
159    row_bytes: usize,
160    tp_rank: usize,
161    tp_size: usize,
162) -> Result<(Vec<CopyOp>, usize)> {
163    ensure!(tp_rank < tp_size, "tp_rank {tp_rank} >= tp_size {tp_size}");
164    let mut ops = Vec::with_capacity(segments.len());
165    let mut src_rows = 0usize;
166    let mut dst_rows = 0usize;
167    for (i, &seg) in segments.iter().enumerate() {
168        ensure!(
169            seg.is_multiple_of(tp_size),
170            "segment {i} ({seg} rows) not divisible by tp_size {tp_size}",
171        );
172        let local = seg / tp_size;
173        ops.push(CopyOp {
174            src_off: (src_rows + tp_rank * local) * row_bytes,
175            dst_off: dst_rows * row_bytes,
176            len: local * row_bytes,
177        });
178        src_rows += seg;
179        dst_rows += local;
180    }
181    Ok((ops, dst_rows))
182}
183
184/// 2026-09-25: Execute a segmented row-slice on the GPU. `row_elems` is the number of
185/// elements per row (`in_dim`); `elem_bytes` its size (2 = BF16, 4 = FP32).
186/// Returns `(local_ptr, local_total_rows)`. For `tp_size <= 1` returns the
187/// source untouched (no allocation, caller must not double-free).
188fn slice_segments(
189    src: DevicePtr,
190    segments: &[usize],
191    row_elems: usize,
192    elem_bytes: usize,
193    tp_rank: usize,
194    tp_size: usize,
195    gpu: &dyn GpuBackend,
196) -> Result<(DevicePtr, usize)> {
197    let full_rows: usize = segments.iter().sum();
198    if tp_size <= 1 {
199        return Ok((src, full_rows));
200    }
201    let row_bytes = row_elems * elem_bytes;
202    let (ops, local_rows) = segment_copy_plan(segments, row_bytes, tp_rank, tp_size)?;
203    let dst = gpu.alloc(local_rows * row_bytes)?;
204    tracing::debug!(
205        target: "metrale_model_arch::tp_shard",
206        ?segments, full_rows, row_elems, elem_bytes, local_rows,
207        tp_rank, tp_size, src = src.0,
208        "gdn segmented row-slice"
209    );
210    for op in &ops {
211        gpu.copy_d2d(src.offset(op.src_off), dst.offset(op.dst_off), op.len)?;
212    }
213    Ok((dst, local_rows))
214}
215
216/// 2026-09-25: Shard the `[Q|K|V]` (`in_proj_qkv`) BF16 weight `[full_conv_dim, h]` to the
217/// local rank's `[local_conv_dim, h]`, slicing Q, K and V independently by the
218/// local head range. Returns `(ptr, local_rows, h)`.
219pub fn shard_gdn_qkv_rows(
220    src: DevicePtr,
221    dims: &TpGdnDims,
222    gpu: &dyn GpuBackend,
223) -> Result<(DevicePtr, usize, usize)> {
224    let (ptr, rows) = slice_segments(
225        src,
226        &dims.qkv_segments(),
227        dims.h,
228        BF16_BYTES,
229        dims.tp_rank,
230        dims.tp_size,
231        gpu,
232    )?;
233    Ok((ptr, rows, dims.h))
234}
235
236/// 2026-09-25: Shard the concatenated `[Q|K|V|Z]` (`in_proj_qkvz`) BF16 weight
237/// `[full_qkvz_out, h]` to the local rank's `[local_qkvz_out, h]`, slicing all
238/// four segments independently. Returns `(ptr, local_rows, h)`.
239pub fn shard_gdn_qkvz_rows(
240    src: DevicePtr,
241    dims: &TpGdnDims,
242    gpu: &dyn GpuBackend,
243) -> Result<(DevicePtr, usize, usize)> {
244    let (ptr, rows) = slice_segments(
245        src,
246        &dims.qkvz_segments(),
247        dims.h,
248        BF16_BYTES,
249        dims.tp_rank,
250        dims.tp_size,
251        gpu,
252    )?;
253    Ok((ptr, rows, dims.h))
254}
255
256/// 2026-09-25: Shard the BA gate BF16 weight `[2*full_nv, h]` to `[2*local_nv, h]`.
257///
258/// `interleave_ba` lays it out per key-head group (`[β₀..β_{vpg-1},
259/// α₀..α_{vpg-1}]` per group, `vpg = nv/nk`). Rank `r` owns key-head groups
260/// `[r*local_nk, (r+1)*local_nk)`, which are the contiguous row range
261/// `[r*2*local_nv, (r+1)*2*local_nv)`, so one contiguous slice keeps the
262/// interleave.
263pub fn shard_gdn_ba_rows(
264    src: DevicePtr,
265    dims: &TpGdnDims,
266    gpu: &dyn GpuBackend,
267) -> Result<(DevicePtr, usize, usize)> {
268    // 2026-09-25: `full_nk` divisible by `tp_size` means each rank gets whole groups.
269    ensure!(
270        dims.full_nk.is_multiple_of(dims.tp_size),
271        "BA: full_nk {} not divisible by tp_size {}",
272        dims.full_nk,
273        dims.tp_size,
274    );
275    let (ptr, rows) = slice_segments(
276        src,
277        &[2 * dims.full_nv],
278        dims.h,
279        BF16_BYTES,
280        dims.tp_rank,
281        dims.tp_size,
282        gpu,
283    )?;
284    Ok((ptr, rows, dims.h))
285}
286
287/// 2026-09-25: Shard the depthwise `conv1d` BF16 weight `[full_conv_dim, d_conv]` to
288/// `[local_conv_dim, d_conv]`. Its channels are the QKV channels (one filter
289/// per channel), so it uses the same `[Q|K|V]` segments as the QKV
290/// in-projection.
291pub fn shard_gdn_conv_rows(
292    src: DevicePtr,
293    dims: &TpGdnDims,
294    d_conv: usize,
295    gpu: &dyn GpuBackend,
296) -> Result<(DevicePtr, usize, usize)> {
297    let (ptr, rows) = slice_segments(
298        src,
299        &dims.qkv_segments(),
300        d_conv,
301        BF16_BYTES,
302        dims.tp_rank,
303        dims.tp_size,
304        gpu,
305    )?;
306    Ok((ptr, rows, d_conv))
307}
308
309/// 2026-09-25: Shard a per-value-head 1D vector on the value-head axis:
310/// `[full_nv * unit]` → `[local_nv * unit]`, where `unit` is the number of
311/// elements per value head and `elem_bytes` their size. The loaders call it for
312/// `a_log` / `dt_bias` (`unit = 1`, `elem_bytes = 4`). Returns
313/// `(ptr, local_len_elems)`.
314pub fn shard_gdn_value_vector(
315    src: DevicePtr,
316    dims: &TpGdnDims,
317    unit: usize,
318    elem_bytes: usize,
319    gpu: &dyn GpuBackend,
320) -> Result<(DevicePtr, usize)> {
321    let full_len = dims.full_nv * unit;
322    if dims.tp_size <= 1 {
323        return Ok((src, full_len));
324    }
325    ensure!(
326        dims.tp_rank < dims.tp_size,
327        "tp_rank {} >= tp_size {}",
328        dims.tp_rank,
329        dims.tp_size,
330    );
331    let local_len = dims.local_nv * unit;
332    let local_bytes = local_len * elem_bytes;
333    let dst = gpu.alloc(local_bytes)?;
334    let src_off = dims.tp_rank * local_bytes;
335    tracing::debug!(
336        target: "metrale_model_arch::tp_shard",
337        full_nv = dims.full_nv, local_nv = dims.local_nv, unit, elem_bytes,
338        full_len, local_len, local_bytes, src_off, tp_rank = dims.tp_rank,
339        src = src.0,
340        "gdn value-vector shard (per-value-head axis)"
341    );
342    gpu.copy_d2d(src.offset(src_off), dst, local_bytes)?;
343    Ok((dst, local_len))
344}
345
346/// 2026-09-25: Shard the `out_proj` BF16 weight `[h, full_value_dim]` row-parallel on its
347/// input dim (value_dim). Rank `r` keeps columns
348/// `[r*local_value_dim, (r+1)*local_value_dim)` of every output row; the layer
349/// sums the partial products with an all-reduce after the GEMM. Returns
350/// `(ptr, h, local_value_dim)`.
351pub fn shard_gdn_out_proj_row_parallel(
352    src: DevicePtr,
353    dims: &TpGdnDims,
354    gpu: &dyn GpuBackend,
355) -> Result<(DevicePtr, usize, usize)> {
356    shard_dense_bf16(
357        src,
358        dims.h,
359        dims.full_value_dim(),
360        TpShardKind::RowParallel,
361        dims.tp_rank,
362        dims.tp_size,
363        gpu,
364    )
365}