metrale_model_layers/layers/
dense_ffn_w8a8.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-28: The declared-W8A8 arm of the dense SiLU FFN (`crate::layers::W8a8Ffn`): the
4//! gate and up projections share one quantized input, and the down projection's input,
5//! `bf16(silu(gate) * up)`, is quantized in the same launch that computes it. Every entry
6//! point (`forward`, `forward_k2`, `forward_k3`, `forward_km`, `forward_prefill`) tries it
7//! first for `1..=ops::W8A8_MAX_ROWS` rows.
8//!
9//! Owner: model-layers (dense FFN).
10//! Invariants:
11//! - On `Ok(true)` the `[rows, hidden]` output is in `ctx.buffers.moe_output()`, as for the
12//!   other arms; `expert_gate_out` / `expert_up_out` hold the gate and up rows.
13//! - `Ok(false)` launches nothing: no W8A8 weights, a LoRA overlay, or a row count outside the
14//!   family.
15
16use anyhow::{Result, ensure};
17use metrale_gpu_runtime::gpu::DevicePtr;
18
19use super::{DenseFfnLayer, FfnActivation};
20use crate::layer::ForwardContext;
21use crate::layers::{W8a8Ffn, ops};
22
23impl DenseFfnLayer {
24    /// 2026-09-28: Install the W8A8 gate, up and down. Refuses a GELU FFN and shapes other than
25    /// `[inter, hidden]` (gate, up) and `[hidden, inter]` (down).
26    pub fn set_w8a8_decode_weights(&mut self, w: W8a8Ffn, hidden: u32, inter: u32) -> Result<()> {
27        ensure!(
28            self.activation == FfnActivation::SiLU,
29            "W8A8 FFN arm fuses SiLU only"
30        );
31        let shapes = [&w.gate, &w.up, &w.down].map(|p| (p.n(), p.k()));
32        ensure!(
33            shapes == [(inter, hidden), (inter, hidden), (hidden, inter)],
34            "W8A8 FFN shapes {shapes:?}, want gate/up [{inter}, {hidden}] and down [{hidden}, {inter}]"
35        );
36        self.w8a8 = Some(w);
37        Ok(())
38    }
39
40    /// 2026-09-28: The W8A8 FFN over `rows` rows of `input` (`[rows, hidden]` BF16).
41    pub(super) fn forward_w8a8(
42        &self,
43        input: DevicePtr,
44        rows: usize,
45        ctx: &ForwardContext,
46        stream: u64,
47    ) -> Result<bool> {
48        let Some(ref w) = self.w8a8 else {
49            return Ok(false);
50        };
51        if self.lora.is_some() || !w.ctx.available(&w.gate, rows) || !w.ctx.available(&w.down, rows)
52        {
53            return Ok(false);
54        }
55        let (h, inter) = (w.gate.k(), w.gate.n());
56        let gate_out = ctx.buffers.expert_gate_out();
57        let up_out = ctx.buffers.expert_up_out();
58        let output = ctx.buffers.moe_output();
59        w.ctx
60            .proj(ctx.gpu, &w.gate, input, h, rows, gate_out, inter, stream)?;
61        // 2026-09-28: `up` reads the activation `gate` quantized into the shared scratch.
62        ops::w8a8_gemv(
63            ctx.gpu,
64            &w.ctx.kernels,
65            &w.up,
66            &w.ctx.scratch,
67            rows,
68            up_out,
69            inter,
70            stream,
71        )?;
72        w.ctx.silu_proj(
73            ctx.gpu, &w.down, gate_out, up_out, inter, rows, output, h, stream,
74        )
75    }
76}