spark_model/layers/ple/dump.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Highway taps for the qwen4_exp bisect.
4//!
5//! Every PIECE of this port is pinned to the reference — the n-gram ids are
6//! bit-exact, the mHC kernels and the PLE gate/conv match to cosine 0.99999,
7//! the NVMe gather is bit-exact — and the model still does not produce
8//! coherent text. That combination says the fault is in the COMPOSITION, and
9//! composition is exactly what per-kernel probes cannot see.
10//!
11//! So: dump the `hc_mult`-wide residual highway at named points and diff it
12//! against the reference layer by layer.
13//!
14//! **The sub-layer boundary is what makes this affordable.** Tapping after a
15//! block's `hc_post` but BEFORE the next `hc_pre` means the reference only has
16//! to reproduce that block — for layer 0 that is the GDN projections alone,
17//! with none of the 512-expert MoE. Only the taps that come after an MoE need
18//! experts, and even then top-10 routing over a short prompt touches a few
19//! dozen, not 512.
20//!
21//! Off unless `ATLAS_QWEN4EXP_DUMP` names a directory. Writes
22//! `<dir>/L{layer:02}_{tag}.bin` as raw little-endian FP32, `[T, hc*H]`.
23
24use spark_runtime::gpu::{DevicePtr, GpuBackend};
25
26/// Directory from `ATLAS_QWEN4EXP_DUMP`, resolved once.
27fn dump_dir() -> Option<&'static str> {
28 static DIR: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
29 DIR.get_or_init(|| {
30 let d = std::env::var("ATLAS_QWEN4EXP_DUMP")
31 .ok()
32 .filter(|s| !s.is_empty());
33 if let Some(ref path) = d {
34 let _ = std::fs::create_dir_all(path);
35 tracing::warn!(
36 "ATLAS_QWEN4EXP_DUMP={path}: taping the mHC highway to disk. \
37 This SYNCHRONIZES and copies D2H at every tap — a debug aid, \
38 not a serving mode."
39 );
40 }
41 d
42 })
43 .as_deref()
44}
45
46/// One-shot: refuse to overwrite a tap that already exists.
47///
48/// `SSM_LAYER_CALL_COUNTER` is a global that never resets, so the second
49/// request of a run labels its taps L36+, the third L72+, and so on. Left
50/// alone, that means a second request silently leaves the FIRST request's
51/// L00 files in place while adding mislabelled ones — and a bisect then
52/// compares a stale tap against a fresh reference and calls it a divergence.
53///
54/// So the first prefill after startup wins and everything later is ignored.
55/// The intended use is exactly that: start the server, send one request,
56/// read the taps.
57fn claim(path: &str) -> bool {
58 !std::path::Path::new(path).exists()
59}
60
61/// Tap the FP32 highway. No-op unless the dump directory is set.
62///
63/// Synchronizes before reading, so it must never run inside CUDA-graph
64/// capture — which is already true of this model's path (`ATLAS_DEBUG_NO_GRAPH`).
65pub fn tap_highway(
66 gpu: &dyn GpuBackend,
67 streams: DevicePtr,
68 layer: usize,
69 tag: &str,
70 num_tokens: usize,
71 hc_dim: usize,
72 stream: u64,
73) {
74 let Some(dir) = dump_dir() else {
75 return;
76 };
77 let path = format!("{dir}/L{layer:02}_{tag}.bin");
78 if !claim(&path) {
79 return;
80 }
81 if gpu.synchronize(stream).is_err() {
82 return;
83 }
84 let mut raw = vec![0u8; num_tokens * hc_dim * 4];
85 if gpu.copy_d2h(streams, &mut raw).is_err() {
86 return;
87 }
88 let path = format!("{dir}/L{layer:02}_{tag}.bin");
89 if let Err(e) = std::fs::write(&path, &raw) {
90 tracing::warn!("highway tap {path}: {e}");
91 }
92}
93
94/// Tap a BF16 buffer (the embedding, a block output) the same way.
95pub fn tap_bf16(
96 gpu: &dyn GpuBackend,
97 ptr: DevicePtr,
98 layer: usize,
99 tag: &str,
100 n_elements: usize,
101 stream: u64,
102) {
103 let Some(dir) = dump_dir() else {
104 return;
105 };
106 let path = format!("{dir}/L{layer:02}_{tag}.bf16.bin");
107 if !claim(&path) {
108 return;
109 }
110 if gpu.synchronize(stream).is_err() {
111 return;
112 }
113 let mut raw = vec![0u8; n_elements * 2];
114 if gpu.copy_d2h(ptr, &mut raw).is_err() {
115 return;
116 }
117 if let Err(e) = std::fs::write(&path, &raw) {
118 tracing::warn!("highway tap {path}: {e}");
119 }
120}
121
122/// Tap an FP32 buffer of `n_elements` (the injection vector, a gate).
123pub fn tap_f32(
124 gpu: &dyn GpuBackend,
125 ptr: DevicePtr,
126 layer: usize,
127 tag: &str,
128 n_elements: usize,
129 stream: u64,
130) {
131 let Some(dir) = dump_dir() else {
132 return;
133 };
134 let path = format!("{dir}/L{layer:02}_{tag}.bin");
135 if !claim(&path) {
136 return;
137 }
138 if gpu.synchronize(stream).is_err() {
139 return;
140 }
141 let mut raw = vec![0u8; n_elements * 4];
142 if gpu.copy_d2h(ptr, &mut raw).is_err() {
143 return;
144 }
145 if let Err(e) = std::fs::write(&path, &raw) {
146 tracing::warn!("highway tap {path}: {e}");
147 }
148}