metrale_model_layers/layers/qwen3_attention/
innerq_driver.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Host-side driver for InnerQ, the per-channel K equalization of
4//! the WHT-rotated (turbo) KV dtypes.
5//!
6//! `TURBO_INNERQ=N` (N > 0 calibration tokens) turns it on;
7//! `TURBO_INNERQ_STRENGTH` in (0, 1] sets the exponent, 0.5 otherwise. The
8//! state is `__device__` globals in namespace `tq_plus`, defined in
9//! `kernels/gb10/common/tq_plus_innerq_apply.cu`. The driver reaches them
10//! through the registry: `device_symbol` for the address, async copies to read
11//! and write.
12//!
13//! `start` zeroes the accumulators and sets `d_innerq_calibrating = 1`.
14//! `maybe_finalize` reads `d_innerq_count`; once it reaches `target_tokens` it
15//! clears `d_innerq_calibrating`, derives per-channel `scale`/`scale_inv` from
16//! `d_innerq_sq_accum`, and either stops (every channel ratio within 1.2x) or
17//! uploads them and sets `d_innerq_active = 1`.
18//!
19//! Owner: model-layers (attention).
20//! Invariants: the `d_innerq_active = 1` write is issued only after the scale
21//! uploads were synchronised on the driver's stream.
22
23use std::ffi::c_void;
24use std::sync::atomic::{AtomicBool, Ordering};
25
26use anyhow::{Context, Result, bail};
27use std::sync::Arc;
28
29use metrale_gpu_runtime::registry::MetraleRegistry;
30
31// 2026-09-25: Itanium-mangled names of the `tq_plus::d_innerq_*` globals,
32// defined in `kernels/gb10/common/tq_plus_innerq_apply.cu`. The module must be
33// that file's: each `.cu` is its own PTX module, built without `-rdc`, so each
34// module has its own copy of a `__device__` global, and only this module's
35// kernels (`tq_plus_innerq_apply_q`, `tq_plus_innerq_apply_k`) read them.
36const MODULE: &str = "tq_plus_innerq_apply";
37const SYM_SCALE: &str = "_ZN7tq_plus14d_innerq_scaleE";
38const SYM_SCALE_INV: &str = "_ZN7tq_plus18d_innerq_scale_invE";
39const SYM_SQ_ACCUM: &str = "_ZN7tq_plus17d_innerq_sq_accumE";
40const SYM_COUNT: &str = "_ZN7tq_plus14d_innerq_countE";
41const SYM_ACTIVE: &str = "_ZN7tq_plus15d_innerq_activeE";
42const SYM_CALIBRATING: &str = "_ZN7tq_plus20d_innerq_calibratingE";
43
44// 2026-09-25: Equal to `INNERQ_MAX_CHANNELS` in `tq_plus_innerq.cuh`; the
45// copies below also check each symbol's real size.
46const MAX_CHANNELS: usize = 128;
47
48pub struct InnerQDriver {
49    /// 2026-09-25: This model's kernel registry. Every device symbol is
50    /// resolved against it, so a driver writes only its own model's globals.
51    registry: Arc<MetraleRegistry>,
52    pub target_tokens: i32,
53    pub strength: f32,
54    pub calibrating: AtomicBool,
55    pub finalized: AtomicBool,
56}
57
58impl InnerQDriver {
59    /// 2026-09-25: Reads `TURBO_INNERQ` and `TURBO_INNERQ_STRENGTH`. Returns
60    /// `None` if `TURBO_INNERQ` is unset, not an `i32`, or `<= 0`. A strength
61    /// outside (0, 1] or unparsable is 0.5.
62    pub fn from_env(registry: Arc<MetraleRegistry>) -> Option<Self> {
63        let n = std::env::var("TURBO_INNERQ")
64            .ok()
65            .and_then(|v| v.parse::<i32>().ok())
66            .filter(|&n| n > 0)?;
67        let strength: f32 = std::env::var("TURBO_INNERQ_STRENGTH")
68            .ok()
69            .and_then(|v| v.parse().ok())
70            .filter(|&s: &f32| s > 0.0 && s <= 1.0)
71            .unwrap_or(0.5);
72        Some(Self {
73            registry,
74            target_tokens: n,
75            strength,
76            calibrating: AtomicBool::new(false),
77            finalized: AtomicBool::new(false),
78        })
79    }
80
81    /// 2026-09-25: Enters calibration: zeroes `d_innerq_sq_accum`,
82    /// `d_innerq_count` and `d_innerq_active`, sets `d_innerq_calibrating = 1`,
83    /// and resets the host flags. Calling it again gives the same state.
84    pub fn start(&self) -> Result<()> {
85        let reg = &self.registry;
86        let stream = reg.raw_stream();
87
88        let zeros_f32 = [0.0f32; MAX_CHANNELS];
89        let zero_i32: i32 = 0;
90        let one_i32: i32 = 1;
91
92        let (sq_ptr, sq_bytes) = reg
93            .device_symbol(MODULE, SYM_SQ_ACCUM)
94            .with_context(|| format!("resolve {MODULE}::{SYM_SQ_ACCUM}"))?;
95        let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
96        let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
97        let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
98
99        let copy_bytes = sq_bytes.min(std::mem::size_of_val(&zeros_f32));
100        // 2026-09-25: SAFETY (all four copies): `copy_h2d_async` requires (a) a
101        // valid device destination, (b) `bytes` readable from `src`, and (c)
102        // `src` alive until the next sync on `stream`.
103        //   (a) every `*_ptr` comes from `device_symbol` on this model's
104        //       registry.
105        //   (b) `copy_bytes = min(sq_bytes, size_of_val(&zeros_f32))` is bounded
106        //       by the symbol's reported length and the 512-byte host array. The
107        //       three scalar copies move `size_of::<i32>()` bytes from `i32`
108        //       locals into `d_innerq_count/_active/_calibrating`, declared
109        //       `int` in tq_plus_innerq.cuh.
110        //   (c) the sources are locals of this function, and the
111        //       `stream_synchronize` after the block retires every copy before
112        //       they go out of scope. If a later copy returns an error, the
113        //       function returns before that sync.
114        unsafe {
115            reg.copy_h2d_async(
116                sq_ptr,
117                zeros_f32.as_ptr() as *const c_void,
118                copy_bytes,
119                stream,
120            )?;
121            reg.copy_h2d_async(
122                count_ptr,
123                &zero_i32 as *const i32 as *const c_void,
124                std::mem::size_of::<i32>(),
125                stream,
126            )?;
127            reg.copy_h2d_async(
128                active_ptr,
129                &zero_i32 as *const i32 as *const c_void,
130                std::mem::size_of::<i32>(),
131                stream,
132            )?;
133            reg.copy_h2d_async(
134                calib_ptr,
135                &one_i32 as *const i32 as *const c_void,
136                std::mem::size_of::<i32>(),
137                stream,
138            )?;
139        }
140        // 2026-09-25: The stack sources must live until the copies retire.
141        reg.stream_synchronize(stream)?;
142
143        self.calibrating.store(true, Ordering::Release);
144        self.finalized.store(false, Ordering::Release);
145        tracing::info!(
146            "InnerQ calibration started: target={} tokens, strength={:.2}",
147            self.target_tokens,
148            self.strength,
149        );
150        Ok(())
151    }
152
153    /// 2026-09-25: Polls `d_innerq_count`. Once it reaches `target_tokens`,
154    /// reads `d_innerq_sq_accum`, computes per-channel `scale`/`scale_inv`,
155    /// uploads them and sets `d_innerq_active = 1`. Returns `Ok(true)` on the
156    /// call that activates and `Ok(false)` otherwise, including when the
157    /// channels are already balanced and nothing is uploaded. Errors when
158    /// `group_size` is outside `1..=MAX_CHANNELS` or larger than a symbol.
159    pub fn maybe_finalize(&self, group_size: i32) -> Result<bool> {
160        if self.finalized.load(Ordering::Acquire) {
161            return Ok(false);
162        }
163        let gs = group_size as usize;
164        if gs == 0 || gs > MAX_CHANNELS {
165            bail!("group_size {group_size} out of range (1..={MAX_CHANNELS})");
166        }
167
168        let reg = &self.registry;
169        let stream = reg.raw_stream();
170
171        let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
172        let mut count: i32 = 0;
173        // 2026-09-25: SAFETY: `count_ptr` is `d_innerq_count`, an `int` in this
174        // model's module; the copy is `size_of::<i32>()` bytes into `count`, a
175        // local that the `stream_synchronize` on the next line keeps alive until
176        // the copy retires. No other reference to `count` exists.
177        unsafe {
178            reg.copy_d2h_async(
179                &mut count as *mut i32 as *mut c_void,
180                count_ptr,
181                std::mem::size_of::<i32>(),
182                stream,
183            )?;
184        }
185        reg.stream_synchronize(stream)?;
186
187        if count < self.target_tokens {
188            return Ok(false);
189        }
190
191        let (sq_ptr, sq_bytes) = reg.device_symbol(MODULE, SYM_SQ_ACCUM)?;
192        let mut sq_accum = [0.0f32; MAX_CHANNELS];
193        let accum_bytes = gs * std::mem::size_of::<f32>();
194        // 2026-09-25: The host side is bounded by the `gs <= MAX_CHANNELS` check
195        // above. The device side is bounded by the symbol's real length from
196        // `device_symbol`, so the check does not depend on `MAX_CHANNELS`
197        // agreeing with `INNERQ_MAX_CHANNELS`.
198        if accum_bytes > sq_bytes {
199            bail!(
200                "{MODULE}::{SYM_SQ_ACCUM} is {sq_bytes} bytes but group_size {group_size} \
201                 needs {accum_bytes} — INNERQ_MAX_CHANNELS and MAX_CHANNELS disagree"
202            );
203        }
204        // 2026-09-25: SAFETY: `copy_d2h_async` needs a valid device source,
205        // `bytes` writable at `dst`, and `dst` alive until the sync. `sq_ptr` is
206        // `d_innerq_sq_accum` from this model's module; `accum_bytes = gs * 4` is
207        // `<= sq_bytes` (checked above) and `<= size_of_val(&sq_accum)` because
208        // `gs <= MAX_CHANNELS`. `sq_accum` is an initialised local that lives to
209        // the end of the function, and the `stream_synchronize` below retires the
210        // copy before it is read.
211        unsafe {
212            reg.copy_d2h_async(
213                sq_accum.as_mut_ptr() as *mut c_void,
214                sq_ptr,
215                accum_bytes,
216                stream,
217            )?;
218        }
219        reg.stream_synchronize(stream)?;
220
221        // 2026-09-25: `scale[i] = (mean_rms / rms[i]) ^ strength`, clamped to
222        // [0.5, 2.0] (ratio 1 for a channel with rms <= 1e-10). Nothing is
223        // uploaded when every ratio is inside (1/1.2, 1.2).
224        let count_f = count as f32;
225        let mut rms = [0.0f32; MAX_CHANNELS];
226        let mut mean_rms = 0.0f32;
227        for i in 0..gs {
228            rms[i] = (sq_accum[i] / count_f).sqrt();
229            mean_rms += rms[i];
230        }
231        mean_rms /= gs as f32;
232
233        let mut scale = [1.0f32; MAX_CHANNELS];
234        let mut scale_inv = [1.0f32; MAX_CHANNELS];
235        let mut max_ratio = 0.0f32;
236        let mut min_ratio = 1e30f32;
237        for i in 0..gs {
238            let ratio = if rms[i] > 1e-10 {
239                mean_rms / rms[i]
240            } else {
241                1.0
242            };
243            let s = ratio.powf(self.strength).clamp(0.5, 2.0);
244            scale[i] = s;
245            scale_inv[i] = 1.0 / s;
246            if ratio > max_ratio {
247                max_ratio = ratio;
248            }
249            if ratio < min_ratio {
250                min_ratio = ratio;
251            }
252        }
253
254        let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
255        let zero_i32: i32 = 0;
256        // 2026-09-25: SAFETY: `calib_ptr` is `d_innerq_calibrating`, an `int` in
257        // this model's module; the copy is `size_of::<i32>()` bytes from
258        // `zero_i32`, a local that stays in scope to the end of the function. The
259        // auto-disable return and the success path both synchronise the stream
260        // first. The error returns between here and the first sync below (a
261        // failed `device_symbol`, the size `bail!`, a failed upload) do not.
262        unsafe {
263            reg.copy_h2d_async(
264                calib_ptr,
265                &zero_i32 as *const i32 as *const c_void,
266                std::mem::size_of::<i32>(),
267                stream,
268            )?;
269        }
270
271        if max_ratio < 1.2 && min_ratio > (1.0 / 1.2) {
272            reg.stream_synchronize(stream)?;
273            self.calibrating.store(false, Ordering::Release);
274            self.finalized.store(true, Ordering::Release);
275            tracing::info!(
276                "InnerQ auto-disabled (channels already balanced: max_ratio={max_ratio:.3}, \
277                 min_ratio={min_ratio:.3})"
278            );
279            return Ok(false);
280        }
281
282        let (scale_ptr, scale_bytes) = reg.device_symbol(MODULE, SYM_SCALE)?;
283        let (scale_inv_ptr, scale_inv_bytes) = reg.device_symbol(MODULE, SYM_SCALE_INV)?;
284        let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
285        let one_i32: i32 = 1;
286        let copy_bytes = gs * std::mem::size_of::<f32>();
287        // 2026-09-25: These two are writes into device globals, where an overrun
288        // would silently overwrite the next global. They are bounded by the
289        // symbols' real lengths, not by `MAX_CHANNELS`.
290        if copy_bytes > scale_bytes || copy_bytes > scale_inv_bytes {
291            bail!(
292                "{MODULE} scale symbols are {scale_bytes}/{scale_inv_bytes} bytes but \
293                 group_size {group_size} needs {copy_bytes} — INNERQ_MAX_CHANNELS and \
294                 MAX_CHANNELS disagree"
295            );
296        }
297        // 2026-09-25: SAFETY: `scale_ptr`/`scale_inv_ptr` are `d_innerq_scale` /
298        // `d_innerq_scale_inv` from this model's module. `copy_bytes = gs * 4` is
299        // `<= scale_bytes`/`scale_inv_bytes` (checked above) and
300        // `<= size_of_val(&scale)` because `gs <= MAX_CHANNELS`. `scale` and
301        // `scale_inv` are initialised locals that stay in scope past the
302        // `stream_synchronize` that retires these copies. If the second copy
303        // returns an error, the function returns before that sync.
304        unsafe {
305            reg.copy_h2d_async(
306                scale_ptr,
307                scale.as_ptr() as *const c_void,
308                copy_bytes,
309                stream,
310            )?;
311            reg.copy_h2d_async(
312                scale_inv_ptr,
313                scale_inv.as_ptr() as *const c_void,
314                copy_bytes,
315                stream,
316            )?;
317        }
318        // 2026-09-25: The scale uploads are synchronised before the
319        // `d_innerq_active` write is issued.
320        reg.stream_synchronize(stream)?;
321        // 2026-09-25: SAFETY: `active_ptr` is `d_innerq_active`, an `int` in this
322        // model's module; the copy is `size_of::<i32>()` bytes from `one_i32`, a
323        // local that outlives the `stream_synchronize` after the block.
324        unsafe {
325            reg.copy_h2d_async(
326                active_ptr,
327                &one_i32 as *const i32 as *const c_void,
328                std::mem::size_of::<i32>(),
329                stream,
330            )?;
331        }
332        reg.stream_synchronize(stream)?;
333
334        self.calibrating.store(false, Ordering::Release);
335        self.finalized.store(true, Ordering::Release);
336        tracing::info!(
337            "InnerQ scales activated (group_size={group_size}, max_ratio={max_ratio:.3}, \
338             strength={:.2})",
339            self.strength,
340        );
341        Ok(true)
342    }
343}