metrale_model_layers/layers/row_tiers.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-27: Row-invariant tier policies: whether a row's arithmetic may depend
4//! on how many rows share its launch.
5//!
6//! By default several ops pick a kernel by row count R, and the kernels sum in
7//! different orders: the W8A16 projections (scalar-order GEMV up to 16 rows, a
8//! tensor-core tile above), the NVFP4 LM head (GEMV below 9 rows, a tile GEMM
9//! above), and the MoE router and experts (two- and three-row kernels, the
10//! pairwise walk, the grouped paths). A row's logits then depend on R, which
11//! depends on the draft count and on which sequences are active, so greedy
12//! output varies across `num_drafts` and across runs at a fixed concurrency.
13//! Two policies make every row's bits independent of R:
14//! - `METRALE_ROW_EXACT_TIERS` (presence, off by default): the W8A16
15//! projections keep the scalar GEMV's order at every R (16-row batch16
16//! chunks above 16 rows).
17//! - Canonical: the W8A16 projections take the tensor-core tile family's order
18//! at every R (`w8a16_gemm_pipelined` and its 32/64-row twins, which sum
19//! identically). 2026-09-27: The default for FP8 MoE checkpoints;
20//! `--no-canonical-tiers` opts out, and `METRALE_CANONICAL_TIERS` (presence)
21//! turns it on for any checkpoint.
22//!
23//! Either one also makes the NVFP4 LM head take its tile GEMM at every R and
24//! the FP8 MoE take the grouped kernels with the per-row router at every R of
25//! two or more (`GroupedRouting::PerRow`, the bits of `MoeLayer::forward`).
26//!
27//! Owner: model-layers.
28//! Invariants:
29//! - The serve publishes the policy of each model before building it
30//! ([`publish_row_tiers`]); a process that never publishes (a test, an
31//! example) reads the two levers once, and runs by rows without them.
32//! - `--no-canonical-tiers` wins over `METRALE_CANONICAL_TIERS`, which wins over
33//! `METRALE_ROW_EXACT_TIERS`.
34
35use std::sync::atomic::{AtomicU8, Ordering};
36
37/// 2026-09-27: The projection order a process runs.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum RowTiers {
40 /// 2026-09-27: Pick by row count (the order before canonical tiers).
41 ByRows,
42 /// 2026-09-27: Scalar-GEMV order at every row count.
43 Exact,
44 /// 2026-09-27: Tensor-core tile order at every row count.
45 Canonical,
46}
47
48/// 2026-09-27: `RowTiers` from the presence of the two levers.
49pub fn row_tiers_from(exact: bool, canonical: bool) -> RowTiers {
50 if canonical {
51 RowTiers::Canonical
52 } else if exact {
53 RowTiers::Exact
54 } else {
55 RowTiers::ByRows
56 }
57}
58
59/// 2026-09-27: A model's policy: `--no-canonical-tiers` keeps the scalar order
60/// if `METRALE_ROW_EXACT_TIERS` is present and the by-rows tiers otherwise; else
61/// a present lever decides; else FP8 MoE checkpoints run canonical and the
62/// others by rows. The dense mixed FP8/NVFP4 checkpoints (Qwen3.8-27B) keep
63/// their tiers: canonical tiers cost them 2-4% of decode throughput.
64pub fn resolve_row_tiers(
65 no_canonical: bool,
66 exact_env: bool,
67 canonical_env: bool,
68 fp8_moe_checkpoint: bool,
69) -> RowTiers {
70 if no_canonical {
71 row_tiers_from(exact_env, false)
72 } else if canonical_env || exact_env {
73 row_tiers_from(exact_env, canonical_env)
74 } else if fp8_moe_checkpoint {
75 RowTiers::Canonical
76 } else {
77 RowTiers::ByRows
78 }
79}
80
81const UNPUBLISHED: u8 = 0;
82
83fn encode(t: RowTiers) -> u8 {
84 match t {
85 RowTiers::ByRows => 1,
86 RowTiers::Exact => 2,
87 RowTiers::Canonical => 3,
88 }
89}
90
91/// 2026-09-27: The policy the serve published for the model it builds.
92static PUBLISHED: AtomicU8 = AtomicU8::new(UNPUBLISHED);
93
94/// 2026-09-27: Publish the policy of the model about to be built. A model swap
95/// publishes again before the next build.
96pub fn publish_row_tiers(t: RowTiers) {
97 PUBLISHED.store(encode(t), Ordering::Relaxed);
98 tracing::info!(
99 "row tiers: {t:?}{}",
100 if t == RowTiers::ByRows {
101 " (kernels picked by row count)"
102 } else {
103 " (one summation order per op at every row count)"
104 }
105 );
106}
107
108/// 2026-09-27: This process's `RowTiers`: the published policy, else the two
109/// levers, read once.
110pub fn row_tiers() -> RowTiers {
111 match PUBLISHED.load(Ordering::Relaxed) {
112 1 => RowTiers::ByRows,
113 2 => RowTiers::Exact,
114 3 => RowTiers::Canonical,
115 _ => {
116 static FALLBACK: std::sync::OnceLock<RowTiers> = std::sync::OnceLock::new();
117 *FALLBACK.get_or_init(|| {
118 row_tiers_from(
119 std::env::var_os("METRALE_ROW_EXACT_TIERS").is_some(),
120 std::env::var_os("METRALE_CANONICAL_TIERS").is_some(),
121 )
122 })
123 }
124 }
125}
126
127/// 2026-09-27: Whether this process runs a row-invariant policy.
128pub fn row_invariant() -> bool {
129 row_tiers() != RowTiers::ByRows
130}
131
132/// 2026-10-01: Whether a prefill pass picks its kernels without regard to its row count,
133/// so a row's bits are the same in a long pass and in a short pass over the same rows (a
134/// warm request replaying a prompt's tail). On under the canonical tiers: the FP8 MoE then
135/// takes the grouped prefill kernels at every row count (`MoeLayer::forward_prefill`),
136/// where it took the per-row decode kernels up to 64 rows. The row-count switches left in
137/// a prefill (W8A16 `pipe128`, the BA-gates twin, the router's register tile, the gate/up
138/// `_w1`/`_w2` entries) choose between kernels that give the same bits.
139pub fn prefill_row_invariant() -> bool {
140 row_tiers() == RowTiers::Canonical
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 /// 2026-09-27: Canonical wins over exact; neither lever keeps the by-rows tiers.
148 #[test]
149 fn canonical_wins_and_absence_keeps_by_rows() {
150 assert_eq!(row_tiers_from(false, false), RowTiers::ByRows);
151 assert_eq!(row_tiers_from(true, false), RowTiers::Exact);
152 assert_eq!(row_tiers_from(false, true), RowTiers::Canonical);
153 assert_eq!(row_tiers_from(true, true), RowTiers::Canonical);
154 }
155
156 /// 2026-09-27: FP8 MoE checkpoints default to canonical and the others to by rows;
157 /// the opt-out beats the canonical lever but keeps an exact one; a lever
158 /// decides for any checkpoint.
159 #[test]
160 fn fp8_defaults_to_canonical_and_the_opt_out_wins() {
161 for fp8 in [false, true] {
162 let default = resolve_row_tiers(false, false, false, fp8);
163 assert_eq!(
164 default,
165 if fp8 {
166 RowTiers::Canonical
167 } else {
168 RowTiers::ByRows
169 }
170 );
171 assert_eq!(resolve_row_tiers(true, false, false, fp8), RowTiers::ByRows);
172 assert_eq!(resolve_row_tiers(true, false, true, fp8), RowTiers::ByRows);
173 assert_eq!(resolve_row_tiers(true, true, true, fp8), RowTiers::Exact);
174 assert_eq!(resolve_row_tiers(false, true, false, fp8), RowTiers::Exact);
175 assert_eq!(
176 resolve_row_tiers(false, false, true, fp8),
177 RowTiers::Canonical
178 );
179 assert_eq!(
180 resolve_row_tiers(false, true, true, fp8),
181 RowTiers::Canonical
182 );
183 }
184 }
185}