1use std::sync::{Mutex, OnceLock};
31
32use anyhow::Result;
33use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
34
35use crate::weight_map::QuantizedWeight;
36
37pub const W4A4_MAX_M: u32 = 32;
40pub const W4A4_WIDE_MAX_M: u32 = 64;
42pub const W4A4_MAX_K: u32 = 32768;
44
45#[derive(Clone, Copy)]
46struct W4a4State {
47 quant: KernelHandle,
48 mx8: KernelHandle,
49 mx16: KernelHandle,
50 mx32: KernelHandle,
51 mx16_nt2: KernelHandle,
54 mx32_nt4: KernelHandle,
55 mx16_ps: KernelHandle,
58 mx32_ps: KernelHandle,
59 sms: u32,
61 mx64: KernelHandle,
63 mx64_nt2: KernelHandle,
64 max_m: u32,
67 aq: DevicePtr,
68 a_scale: DevicePtr,
69 a_gs: DevicePtr,
70 audit_ref: DevicePtr,
73}
74
75const AUDIT_MAX_N: usize = 65536;
78
79fn audit_enabled() -> bool {
86 static ON: OnceLock<bool> = OnceLock::new();
87 *ON.get_or_init(|| std::env::var_os("METRALE_W4A4_PROJ_AUDIT").is_some_and(|v| !v.is_empty()))
88}
89
90#[path = "w4a4_proj/mx_plan.rs"]
93mod mx_plan;
94pub use mx_plan::*;
95#[path = "w4a4_proj/fixed.rs"]
96mod fixed;
97pub use fixed::{fixed_nvfp4_proj, nvfp4_proj_mx};
98#[path = "w4a4_proj/steps.rs"]
99mod steps;
100pub use steps::{Nvfp4ActBuf, W4a4Proj};
101
102fn cache() -> &'static Mutex<Vec<(usize, Option<W4a4State>)>> {
103 static CACHE: OnceLock<Mutex<Vec<(usize, Option<W4a4State>)>>> = OnceLock::new();
104 CACHE.get_or_init(|| Mutex::new(Vec::new()))
105}
106
107fn key(gpu: &dyn GpuBackend) -> usize {
108 gpu as *const dyn GpuBackend as *const () as usize
109}
110
111pub fn prepare(gpu: &dyn GpuBackend) -> Result<()> {
116 let tier = crate::layers::weight_quantization();
117 let fixed = crate::layers::any_fixed();
120 if !tier.uses_w4a4_decode() && !fixed {
121 return Ok(());
122 }
123 let mut guard = cache().lock().unwrap_or_else(|p| p.into_inner());
124 if guard.iter().any(|(k, _)| *k == key(gpu)) {
125 return Ok(());
126 }
127 let h = |f: &str| crate::layers::try_kernel(gpu, "w4a4_gemv_mx", f);
128 let (quant, mx8, mx16, mx32) = (
129 h("w4a4_quant_rows"),
130 h("w4a4_gemv_mx8"),
131 h("w4a4_gemv_mx16"),
132 h("w4a4_gemv_mx32"),
133 );
134 let (mx16_nt2, mx32_nt4) = (h("w4a4_gemv_mx16_nt2"), h("w4a4_gemv_mx32_nt4"));
135 let (mx16_ps, mx32_ps) = (h("w4a4_gemv_mx16_ps"), h("w4a4_gemv_mx32_ps"));
136 let state = if [quant, mx8, mx16, mx32, mx16_nt2, mx32_nt4, mx16_ps, mx32_ps]
137 .iter()
138 .all(|k| k.0 != 0)
139 {
140 let sms = gpu.sm_count()?;
141 let (mx64, mx64_nt2, max_m) =
142 wide_entries(tier, fixed, h("w4a4_gemv_mx64"), h("w4a4_gemv_mx64_nt2"));
143 tracing::info!(
144 "w4a4 projection ({tier:?}): METRALE_W4A4_MX_NT={} METRALE_W4A4_MX_PS={} ({sms} SMs), max rows {max_m}",
145 mx_nt(),
146 u8::from(mx_ps()),
147 );
148 let (m, k) = (max_m as usize, W4A4_MAX_K as usize);
149 Some(W4a4State {
150 quant,
151 mx8,
152 mx16,
153 mx32,
154 mx16_nt2,
155 mx32_nt4,
156 mx16_ps,
157 mx32_ps,
158 sms,
159 mx64,
160 mx64_nt2,
161 max_m,
162 aq: gpu.alloc(m * k / 2)?,
163 a_scale: gpu.alloc(m * k / 16)?,
164 a_gs: gpu.alloc(m * 4)?,
165 audit_ref: if audit_enabled() {
166 gpu.alloc(m * AUDIT_MAX_N * 2)?
167 } else {
168 DevicePtr::NULL
169 },
170 })
171 } else {
172 tracing::info!("w4a4_gemv_mx kernels are not in this target; NVFP4 projections run W4A16");
173 None
174 };
175 guard.push((key(gpu), state));
176 Ok(())
177}
178
179fn wide_entries(
185 tier: metrale_config::WeightQuantTier,
186 fixed: bool,
187 mx64: KernelHandle,
188 mx64_nt2: KernelHandle,
189) -> (KernelHandle, KernelHandle, u32) {
190 use metrale_config::{W4a4Downcast, WeightQuantization};
191 if fixed && mx64.0 != 0 && mx64_nt2.0 != 0 {
192 return (mx64, mx64_nt2, W4A4_WIDE_MAX_M);
193 }
194 match (tier.tier(), tier.downcast()) {
195 (WeightQuantization::Declared, _) if mx64.0 != 0 && mx64_nt2.0 != 0 => {
196 (mx64, mx64_nt2, W4A4_WIDE_MAX_M)
197 }
198 (WeightQuantization::Nvfp4, W4a4Downcast::Wide) => (mx64, mx64_nt2, W4A4_WIDE_MAX_M),
199 _ => (KernelHandle(0), KernelHandle(0), W4A4_MAX_M),
200 }
201}
202
203pub fn weight_rows(gpu: &dyn GpuBackend, weight: &QuantizedWeight) -> u32 {
206 rows_for(weight, max_rows(gpu))
207}
208
209fn rows_for(weight: &QuantizedWeight, kernel_rows: u32) -> u32 {
210 crate::layers::weight_quantization().w4a4_rows(
211 weight.act,
212 W4A4_MAX_M,
213 W4A4_WIDE_MAX_M,
214 kernel_rows,
215 )
216}
217
218pub fn max_rows(gpu: &dyn GpuBackend) -> u32 {
221 state(gpu).map_or(0, |s| s.max_m)
222}
223
224fn state(gpu: &dyn GpuBackend) -> Option<W4a4State> {
225 let guard = cache().lock().unwrap_or_else(|p| p.into_inner());
226 guard
227 .iter()
228 .find(|(k, _)| *k == key(gpu))
229 .and_then(|(_, s)| *s)
230}
231
232pub fn w4a4_route(m: u32, n: u32, k: u32, max_m: u32) -> bool {
234 (1..=max_m).contains(&m) && n > 0 && k > 0 && k.is_multiple_of(64) && k <= W4A4_MAX_K
235}
236
237#[allow(clippy::too_many_arguments)]
243#[track_caller]
244pub fn nvfp4_proj_small_m(
245 gpu: &dyn GpuBackend,
246 batch_kernel: KernelHandle,
247 input: DevicePtr,
248 weight: &QuantizedWeight,
249 output: DevicePtr,
250 m: u32,
251 n: u32,
252 k: u32,
253 stream: u64,
254) -> Result<()> {
255 proj(
256 gpu,
257 batch_kernel,
258 input,
259 weight,
260 output,
261 m,
262 n,
263 k,
264 stream,
265 false,
266 )
267}
268
269#[allow(clippy::too_many_arguments)]
276#[track_caller]
277pub fn nvfp4_proj_small_m_same_input(
278 gpu: &dyn GpuBackend,
279 batch_kernel: KernelHandle,
280 input: DevicePtr,
281 weight: &QuantizedWeight,
282 output: DevicePtr,
283 m: u32,
284 n: u32,
285 k: u32,
286 stream: u64,
287) -> Result<()> {
288 proj(
289 gpu,
290 batch_kernel,
291 input,
292 weight,
293 output,
294 m,
295 n,
296 k,
297 stream,
298 true,
299 )
300}
301
302fn w4a4_state_for(
306 gpu: &dyn GpuBackend,
307 weight: &QuantizedWeight,
308 m: u32,
309 n: u32,
310 k: u32,
311) -> Option<W4a4State> {
312 if !crate::layers::weight_quantization().uses_w4a4_decode() {
313 return None;
314 }
315 state(gpu).filter(|s| w4a4_route(m, n, k, rows_for(weight, s.max_m)))
316}
317
318pub fn routes_w4a4(gpu: &dyn GpuBackend, weight: &QuantizedWeight, m: u32, n: u32, k: u32) -> bool {
321 w4a4_state_for(gpu, weight, m, n, k).is_some()
322}
323
324type QuantKey = (usize, u64, u32, u32, u64);
326
327fn last_quant() -> &'static Mutex<Option<QuantKey>> {
328 static LAST: OnceLock<Mutex<Option<QuantKey>>> = OnceLock::new();
329 LAST.get_or_init(|| Mutex::new(None))
330}
331
332#[allow(clippy::too_many_arguments)]
333#[track_caller]
334fn proj(
335 gpu: &dyn GpuBackend,
336 batch_kernel: KernelHandle,
337 input: DevicePtr,
338 weight: &QuantizedWeight,
339 output: DevicePtr,
340 m: u32,
341 n: u32,
342 k: u32,
343 stream: u64,
344 same_input: bool,
345) -> Result<()> {
346 let tier = crate::layers::weight_quantization();
347 if let Some(s) = w4a4_state_for(gpu, weight, m, n, k) {
348 let p = W4a4Proj(s);
349 let want: QuantKey = (key(gpu), input.0, m, k, stream);
350 let mut last = last_quant().lock().unwrap_or_else(|p| p.into_inner());
351 if !(same_input && *last == Some(want)) {
352 p.quantize(gpu, input, p.scratch(), m, k, stream)?;
353 *last = Some(want);
354 }
355 drop(last);
356 p.gemv(gpu, p.scratch(), weight, output, m, n, k, stream)?;
357 if audit_enabled()
358 && !s.audit_ref.is_null()
359 && (n as usize) <= AUDIT_MAX_N
360 && !gpu.stream_is_capturing(stream)
361 {
362 let site = std::panic::Location::caller();
363 super::w4a16_gemv_batchm(
364 gpu,
365 batch_kernel,
366 input,
367 weight,
368 s.audit_ref,
369 m,
370 n,
371 k,
372 stream,
373 )?;
374 audit(
375 gpu,
376 output,
377 s.audit_ref,
378 (m * n) as usize,
379 stream,
380 site,
381 n,
382 k,
383 )?;
384 }
385 return Ok(());
386 }
387 if !same_input && tier.tier() == metrale_config::WeightQuantization::Declared {
391 *last_quant().lock().unwrap_or_else(|p| p.into_inner()) = None;
392 }
393 anyhow::ensure!(
394 m <= W4A4_MAX_M,
395 "w4a4: {m} rows reached the W4A16 fallback, which covers at most {W4A4_MAX_M}"
396 );
397 super::w4a16_gemv_batchm(gpu, batch_kernel, input, weight, output, m, n, k, stream)
398}
399
400#[allow(clippy::too_many_arguments)]
402fn audit(
403 gpu: &dyn GpuBackend,
404 got: DevicePtr,
405 reference: DevicePtr,
406 elems: usize,
407 stream: u64,
408 site: &'static std::panic::Location<'static>,
409 n: u32,
410 k: u32,
411) -> Result<()> {
412 type Acc = std::collections::HashMap<String, (u64, f64, f64)>;
413 static ACC: OnceLock<Mutex<Acc>> = OnceLock::new();
414 gpu.synchronize(stream)?;
415 let mut a = vec![0u8; elems * 2];
416 let mut b = vec![0u8; elems * 2];
417 gpu.copy_d2h(got, &mut a)?;
418 gpu.copy_d2h(reference, &mut b)?;
419 let bf = |x: &[u8]| f32::from_bits((u16::from_le_bytes([x[0], x[1]]) as u32) << 16) as f64;
420 let (mut num, mut den) = (0f64, 0f64);
421 for (x, y) in a.chunks_exact(2).zip(b.chunks_exact(2)) {
422 let (x, y) = (bf(x), bf(y));
423 num += (x - y) * (x - y);
424 den += y * y;
425 }
426 let rel = if den > 0.0 { (num / den).sqrt() } else { 0.0 };
427 let key = format!("{}:{} N={n} K={k}", site.file(), site.line());
428 let mut acc = ACC
429 .get_or_init(|| Mutex::new(Acc::new()))
430 .lock()
431 .unwrap_or_else(|p| p.into_inner());
432 let e = acc.entry(key.clone()).or_insert((0, 0.0, 0.0));
433 e.0 += 1;
434 e.1 += rel;
435 e.2 = e.2.max(rel);
436 if e.0.is_multiple_of(64) {
437 tracing::info!(
438 "W4A4_AUDIT site={key} samples={} mean_rel={:.5} max_rel={:.5}",
439 e.0,
440 e.1 / e.0 as f64,
441 e.2
442 );
443 }
444 Ok(())
445}
446
447#[cfg(test)]
448#[path = "w4a4_proj_tests.rs"]
449mod w4a4_proj_tests;