spark_model/layers/qwen3_attention/
innerq_driver.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Host-side driver for TurboQuant+ InnerQ per-channel K equalization.
4//!
5//! Triggers via `TURBO_INNERQ=N` env var (N = calibration token count). The
6//! kernel-side state lives in `kernels/gb10/common/tq_plus_innerq_apply.cu`
7//! as `__device__` globals inside `namespace tq_plus` — deliberately in the
8//! SAME translation unit (= same PTX module; Atlas has no `-rdc` device
9//! linking) as the apply/accumulate kernels that read it. This driver
10//! manipulates that state directly via the CUDA Driver API:
11//!
12//!   `cuModuleGetGlobal_v2` → device pointer for each symbol
13//!   `cuMemcpyHtoDAsync_v2` / `cuMemcpyDtoHAsync_v2` → push/pull state
14//!
15//! Two-phase operation:
16//!   1. `start()`     — zero counters, set `d_innerq_calibrating = 1`.
17//!   2. `maybe_finalize()` — read `d_innerq_count`; once it crosses
18//!      `target_tokens`, read `d_innerq_sq_accum`, compute per-channel
19//!      scale + scale_inv, upload, set `d_innerq_active = 1`.
20//!
21//! Math identity: `<Q/s, s·K> = <Q, K>` — the kernel-side apply pass
22//! multiplies Q by `scale_inv` pre-WHT and K by `scale` post-WHT, leaving
23//! attention dot products unchanged while smoothing K variance across
24//! channels.
25
26use std::ffi::c_void;
27use std::sync::atomic::{AtomicBool, Ordering};
28
29use anyhow::{Context, Result, bail};
30use std::sync::Arc;
31
32use atlas_core::registry::AtlasRegistry;
33
34// Itanium-mangled names for `tq_plus::*` device globals. The kernel TU is
35// `kernels/gb10/common/tq_plus_innerq_apply.cu` — the module that also holds
36// `tq_plus_innerq_apply_q/_k`, the only kernels reading this state (module =
37// file stem; no [modules] override in common/KERNEL.toml). It MUST be that
38// module: each PTX module gets its own copy of `__device__` globals (no
39// -rdc), so uploading anywhere else feeds a copy the kernels never see.
40const MODULE: &str = "tq_plus_innerq_apply";
41const SYM_SCALE: &str = "_ZN7tq_plus14d_innerq_scaleE";
42const SYM_SCALE_INV: &str = "_ZN7tq_plus18d_innerq_scale_invE";
43const SYM_SQ_ACCUM: &str = "_ZN7tq_plus17d_innerq_sq_accumE";
44const SYM_COUNT: &str = "_ZN7tq_plus14d_innerq_countE";
45const SYM_ACTIVE: &str = "_ZN7tq_plus15d_innerq_activeE";
46const SYM_CALIBRATING: &str = "_ZN7tq_plus20d_innerq_calibratingE";
47
48// Matches INNERQ_MAX_CHANNELS in tq_plus_innerq.cuh. Head dim = 128 today.
49const MAX_CHANNELS: usize = 128;
50
51pub struct InnerQDriver {
52    /// This model's kernel modules. Held rather than fetched from a global:
53    /// the device symbols below live in THESE modules, and a swapped-in model's
54    /// driver must never resolve them against the previous model's.
55    registry: Arc<AtlasRegistry>,
56    pub target_tokens: i32,
57    pub strength: f32,
58    pub calibrating: AtomicBool,
59    pub finalized: AtomicBool,
60}
61
62impl InnerQDriver {
63    /// Reads `TURBO_INNERQ` and `TURBO_INNERQ_STRENGTH` env vars. Returns
64    /// `None` if `TURBO_INNERQ` is unset, unparsable, or `<= 0`.
65    pub fn from_env(registry: Arc<AtlasRegistry>) -> Option<Self> {
66        let n = std::env::var("TURBO_INNERQ")
67            .ok()
68            .and_then(|v| v.parse::<i32>().ok())
69            .filter(|&n| n > 0)?;
70        let strength: f32 = std::env::var("TURBO_INNERQ_STRENGTH")
71            .ok()
72            .and_then(|v| v.parse().ok())
73            .filter(|&s: &f32| s > 0.0 && s <= 1.0)
74            .unwrap_or(0.5);
75        Some(Self {
76            registry,
77            target_tokens: n,
78            strength,
79            calibrating: AtomicBool::new(false),
80            finalized: AtomicBool::new(false),
81        })
82    }
83
84    /// Enter calibration phase: zero `d_innerq_sq_accum` / `d_innerq_count`
85    /// / `d_innerq_active`, set `d_innerq_calibrating = 1`. Idempotent.
86    pub fn start(&self) -> Result<()> {
87        let reg = &self.registry;
88        let stream = reg.raw_stream();
89
90        let zeros_f32 = [0.0f32; MAX_CHANNELS];
91        let zero_i32: i32 = 0;
92        let one_i32: i32 = 1;
93
94        let (sq_ptr, sq_bytes) = reg
95            .device_symbol(MODULE, SYM_SQ_ACCUM)
96            .with_context(|| format!("resolve {MODULE}::{SYM_SQ_ACCUM}"))?;
97        let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
98        let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
99        let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
100
101        let copy_bytes = sq_bytes.min(std::mem::size_of_val(&zeros_f32));
102        // SAFETY (all four copies): `copy_h2d_async` requires (a) a valid device
103        // destination, (b) `bytes` readable from `src`, and (c) `src` alive until
104        // the next sync on `stream`.
105        //   (a) every `*_ptr` came from `device_symbol()` above, i.e. straight from
106        //       `cuModuleGetGlobal_v2` on THIS model's loaded module (the
107        //       `self.registry` field exists precisely so a hot-swapped model
108        //       cannot resolve against the previous model's copy).
109        //   (b) `copy_bytes = min(sq_bytes, size_of_val(&zeros_f32))` is bounded by
110        //       BOTH the device symbol's reported length and the 512-byte host
111        //       array, so neither side can be over-run. The three scalar copies
112        //       move `size_of::<i32>()` out of `&i32` locals — exact by
113        //       construction — into `d_innerq_count/_active/_calibrating`, which
114        //       are `__device__ int` in tq_plus_innerq.cuh.
115        //   (c) `zeros_f32`, `zero_i32` and `one_i32` are stack locals of this fn
116        //       and the `stream_synchronize` immediately after the block retires
117        //       every copy before they go out of scope.
118        unsafe {
119            reg.copy_h2d_async(
120                sq_ptr,
121                zeros_f32.as_ptr() as *const c_void,
122                copy_bytes,
123                stream,
124            )?;
125            reg.copy_h2d_async(
126                count_ptr,
127                &zero_i32 as *const i32 as *const c_void,
128                std::mem::size_of::<i32>(),
129                stream,
130            )?;
131            reg.copy_h2d_async(
132                active_ptr,
133                &zero_i32 as *const i32 as *const c_void,
134                std::mem::size_of::<i32>(),
135                stream,
136            )?;
137            reg.copy_h2d_async(
138                calib_ptr,
139                &one_i32 as *const i32 as *const c_void,
140                std::mem::size_of::<i32>(),
141                stream,
142            )?;
143        }
144        // Stack locals must live until the copies retire.
145        reg.stream_synchronize(stream)?;
146
147        self.calibrating.store(true, Ordering::Release);
148        self.finalized.store(false, Ordering::Release);
149        tracing::info!(
150            "InnerQ calibration started: target={} tokens, strength={:.2}",
151            self.target_tokens,
152            self.strength,
153        );
154        Ok(())
155    }
156
157    /// Poll `d_innerq_count`. When it crosses `target_tokens`, pull
158    /// `d_innerq_sq_accum`, compute per-channel scale/scale_inv, upload,
159    /// and flip `d_innerq_active = 1`. Returns `Ok(true)` on the call
160    /// that activates, `Ok(false)` on every other call (including
161    /// auto-disable when channels are already balanced).
162    pub fn maybe_finalize(&self, group_size: i32) -> Result<bool> {
163        if self.finalized.load(Ordering::Acquire) {
164            return Ok(false);
165        }
166        let gs = group_size as usize;
167        if gs == 0 || gs > MAX_CHANNELS {
168            bail!("group_size {group_size} out of range (1..={MAX_CHANNELS})");
169        }
170
171        let reg = &self.registry;
172        let stream = reg.raw_stream();
173
174        let (count_ptr, _) = reg.device_symbol(MODULE, SYM_COUNT)?;
175        let mut count: i32 = 0;
176        // SAFETY: `count_ptr` is `d_innerq_count`, a `__device__ int`, resolved from
177        // this model's own module; the transfer is exactly `size_of::<i32>()` bytes
178        // into `&mut count`, a live stack local that outlives the copy — the
179        // `stream_synchronize` on the next line retires the DMA before `count` is
180        // read. `&mut count` is the only reference to it here.
181        unsafe {
182            reg.copy_d2h_async(
183                &mut count as *mut i32 as *mut c_void,
184                count_ptr,
185                std::mem::size_of::<i32>(),
186                stream,
187            )?;
188        }
189        reg.stream_synchronize(stream)?;
190
191        if count < self.target_tokens {
192            return Ok(false);
193        }
194
195        let (sq_ptr, sq_bytes) = reg.device_symbol(MODULE, SYM_SQ_ACCUM)?;
196        let mut sq_accum = [0.0f32; MAX_CHANNELS];
197        let accum_bytes = gs * std::mem::size_of::<f32>();
198        // The host side is bounded by the `gs <= MAX_CHANNELS` check above, but the
199        // DEVICE side is only bounded by `MAX_CHANNELS == INNERQ_MAX_CHANNELS` in
200        // tq_plus_innerq.cuh — a constant in a different language in a different
201        // tree. `device_symbol` hands back the symbol's real length; use it rather
202        // than assume the two constants are still in step.
203        if accum_bytes > sq_bytes {
204            bail!(
205                "{MODULE}::{SYM_SQ_ACCUM} is {sq_bytes} bytes but group_size {group_size} \
206                 needs {accum_bytes} — INNERQ_MAX_CHANNELS and MAX_CHANNELS disagree"
207            );
208        }
209        // SAFETY: `copy_d2h_async` needs a valid device source, `bytes` writable at
210        // `dst`, and `dst` alive until the sync. `sq_ptr` is `d_innerq_sq_accum`
211        // from this model's module; `accum_bytes = gs * 4` is `<= sq_bytes` (checked
212        // immediately above) on the device side and `<= MAX_CHANNELS * 4 =
213        // size_of_val(&sq_accum)` on the host side, since `gs <= MAX_CHANNELS` was
214        // enforced at the top of this fn. `sq_accum` is a fully-initialised stack
215        // array that lives until the end of the fn, and the `stream_synchronize`
216        // below retires the copy before it is read.
217        unsafe {
218            reg.copy_d2h_async(
219                sq_accum.as_mut_ptr() as *mut c_void,
220                sq_ptr,
221                accum_bytes,
222                stream,
223            )?;
224        }
225        reg.stream_synchronize(stream)?;
226
227        // Identity-preserving equalization: scale[i] = (mean_rms / rms[i])
228        // ^strength, clamped to [0.5, 2.0]; auto-disable if max/min ratio
229        // < 1.2 either way.
230        let count_f = count as f32;
231        let mut rms = [0.0f32; MAX_CHANNELS];
232        let mut mean_rms = 0.0f32;
233        for i in 0..gs {
234            rms[i] = (sq_accum[i] / count_f).sqrt();
235            mean_rms += rms[i];
236        }
237        mean_rms /= gs as f32;
238
239        let mut scale = [1.0f32; MAX_CHANNELS];
240        let mut scale_inv = [1.0f32; MAX_CHANNELS];
241        let mut max_ratio = 0.0f32;
242        let mut min_ratio = 1e30f32;
243        for i in 0..gs {
244            let ratio = if rms[i] > 1e-10 {
245                mean_rms / rms[i]
246            } else {
247                1.0
248            };
249            let s = ratio.powf(self.strength).clamp(0.5, 2.0);
250            scale[i] = s;
251            scale_inv[i] = 1.0 / s;
252            if ratio > max_ratio {
253                max_ratio = ratio;
254            }
255            if ratio < min_ratio {
256                min_ratio = ratio;
257            }
258        }
259
260        let (calib_ptr, _) = reg.device_symbol(MODULE, SYM_CALIBRATING)?;
261        let zero_i32: i32 = 0;
262        // SAFETY: `calib_ptr` is `d_innerq_calibrating`, a `__device__ int` in this
263        // model's module; the copy is exactly `size_of::<i32>()` bytes out of
264        // `&zero_i32`. `zero_i32` is declared on the line above and stays in scope
265        // to the end of the fn, so it outlives the copy on BOTH exits: the
266        // auto-disable branch syncs before returning, and the normal path syncs
267        // after the scale uploads below.
268        unsafe {
269            reg.copy_h2d_async(
270                calib_ptr,
271                &zero_i32 as *const i32 as *const c_void,
272                std::mem::size_of::<i32>(),
273                stream,
274            )?;
275        }
276
277        if max_ratio < 1.2 && min_ratio > (1.0 / 1.2) {
278            reg.stream_synchronize(stream)?;
279            self.calibrating.store(false, Ordering::Release);
280            self.finalized.store(true, Ordering::Release);
281            tracing::info!(
282                "InnerQ auto-disabled (channels already balanced: max_ratio={max_ratio:.3}, \
283                 min_ratio={min_ratio:.3})"
284            );
285            return Ok(false);
286        }
287
288        let (scale_ptr, scale_bytes) = reg.device_symbol(MODULE, SYM_SCALE)?;
289        let (scale_inv_ptr, scale_inv_bytes) = reg.device_symbol(MODULE, SYM_SCALE_INV)?;
290        let (active_ptr, _) = reg.device_symbol(MODULE, SYM_ACTIVE)?;
291        let one_i32: i32 = 1;
292        let copy_bytes = gs * std::mem::size_of::<f32>();
293        // These two are WRITES into device globals — an over-run corrupts whatever
294        // the linker placed after them in the module's global segment, silently.
295        // Bound against the symbols' real lengths rather than against
296        // MAX_CHANNELS's agreement with INNERQ_MAX_CHANNELS.
297        if copy_bytes > scale_bytes || copy_bytes > scale_inv_bytes {
298            bail!(
299                "{MODULE} scale symbols are {scale_bytes}/{scale_inv_bytes} bytes but \
300                 group_size {group_size} needs {copy_bytes} — INNERQ_MAX_CHANNELS and \
301                 MAX_CHANNELS disagree"
302            );
303        }
304        // SAFETY: `scale_ptr`/`scale_inv_ptr` are `d_innerq_scale` /
305        // `d_innerq_scale_inv` from this model's module. `copy_bytes = gs * 4` is
306        // `<= scale_bytes`/`scale_inv_bytes` on the device side (checked directly
307        // above) and `<= MAX_CHANNELS * 4 = size_of_val(&scale)` on the host side
308        // (`gs <= MAX_CHANNELS`, enforced at the top of this fn). `scale` and
309        // `scale_inv` are `[f32; MAX_CHANNELS]` stack arrays initialised to 1.0 at
310        // declaration and fully written for indices `0..gs` by the loop above; they
311        // stay in scope past the `stream_synchronize` that retires these copies.
312        unsafe {
313            reg.copy_h2d_async(
314                scale_ptr,
315                scale.as_ptr() as *const c_void,
316                copy_bytes,
317                stream,
318            )?;
319            reg.copy_h2d_async(
320                scale_inv_ptr,
321                scale_inv.as_ptr() as *const c_void,
322                copy_bytes,
323                stream,
324            )?;
325        }
326        // Order: scale uploads must retire before active flips so any kernel
327        // observing active=1 sees the finalized scales (cuMemcpyAsync within
328        // the same stream is already strict-order, but the active flag is
329        // visible to kernels on OTHER streams once a host-side sync passes).
330        reg.stream_synchronize(stream)?;
331        // SAFETY: `active_ptr` is `d_innerq_active`, a `__device__ int` in this
332        // model's module; the copy is exactly `size_of::<i32>()` bytes out of
333        // `&one_i32`, a stack local declared above that outlives the
334        // `stream_synchronize` on the line after.
335        unsafe {
336            reg.copy_h2d_async(
337                active_ptr,
338                &one_i32 as *const i32 as *const c_void,
339                std::mem::size_of::<i32>(),
340                stream,
341            )?;
342        }
343        reg.stream_synchronize(stream)?;
344
345        self.calibrating.store(false, Ordering::Release);
346        self.finalized.store(true, Ordering::Release);
347        tracing::info!(
348            "InnerQ scales activated (group_size={group_size}, max_ratio={max_ratio:.3}, \
349             strength={:.2})",
350            self.strength,
351        );
352        Ok(true)
353    }
354}