use anyhow::{Context, Result};
use mlx_native::{EncoderSession, MlxBuffer, MlxDevice};
use std::cell::Cell;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use super::gpu_full_attn::download_f32;
thread_local! {
static ACTIVE_SESSION: Cell<Option<*mut EncoderSession>> = const { Cell::new(None) };
}
pub fn set_active_session(sess: &mut EncoderSession) {
ACTIVE_SESSION.with(|c| c.set(Some(sess as *mut EncoderSession)));
}
pub fn clear_active_session() {
ACTIVE_SESSION.with(|c| c.set(None));
}
struct DumpConfig {
filter: LayerFilter,
dir: PathBuf,
}
#[derive(Clone, Copy, Debug)]
enum LayerFilter {
All,
Only(usize),
}
static CONFIG: OnceLock<Option<DumpConfig>> = OnceLock::new();
static STEP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn config() -> Option<&'static DumpConfig> {
CONFIG
.get_or_init(|| {
let raw = std::env::var("HF2Q_DUMP_LAYER").ok()?;
let raw = raw.trim();
if raw.is_empty() {
return None;
}
let filter = if raw.eq_ignore_ascii_case("ALL") {
LayerFilter::All
} else {
match raw.parse::<usize>() {
Ok(n) => LayerFilter::Only(n),
Err(_) => {
eprintln!(
"[DUMP_BISECT] HF2Q_DUMP_LAYER={raw:?} is not 'ALL' or a usize; \
dumping disabled"
);
return None;
}
}
};
let run_id = std::env::var("HF2Q_DUMP_RUN_ID")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| std::process::id().to_string());
let dir = PathBuf::from("/tmp/hf2q-dump").join(&run_id);
if let Err(e) = std::fs::create_dir_all(&dir) {
eprintln!(
"[DUMP_BISECT] failed to create {}: {e}; dumping disabled",
dir.display()
);
return None;
}
let manifest_path = dir.join("manifest.txt");
if let Err(e) = File::create(&manifest_path) {
eprintln!(
"[DUMP_BISECT] failed to create manifest {}: {e}; dumping disabled",
manifest_path.display()
);
return None;
}
eprintln!(
"[DUMP_BISECT] enabled: filter={:?} dir={}",
filter,
dir.display()
);
Some(DumpConfig { filter, dir })
})
.as_ref()
}
#[inline]
pub fn is_enabled() -> bool {
config().is_some()
}
#[inline]
pub fn should_dump(layer_idx: Option<usize>) -> bool {
let Some(cfg) = config() else { return false };
match (cfg.filter, layer_idx) {
(LayerFilter::All, _) => true,
(LayerFilter::Only(_), None) => true,
(LayerFilter::Only(n), Some(idx)) => idx == n,
}
}
pub fn next_step() -> u64 {
STEP_COUNTER.fetch_add(1, Ordering::SeqCst)
}
pub fn current_step() -> u64 {
STEP_COUNTER.load(Ordering::SeqCst)
}
#[cfg(test)]
pub fn reset_step() {
STEP_COUNTER.store(0, Ordering::SeqCst);
}
thread_local! {
static CURRENT_LAYER: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
static CURRENT_STEP: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
pub fn current_layer_idx() -> Option<usize> {
CURRENT_LAYER.with(|c| c.get())
}
pub fn current_step_idx() -> u64 {
CURRENT_STEP.with(|c| c.get())
}
pub fn set_current_layer(step: u64, layer_idx: usize) {
CURRENT_STEP.with(|c| c.set(step));
CURRENT_LAYER.with(|c| c.set(Some(layer_idx)));
}
pub fn clear_current_layer() {
CURRENT_LAYER.with(|c| c.set(None));
}
pub fn dump_in_layer(op: &str, buf: &MlxBuffer, shape: &[usize], device: &MlxDevice) {
let layer = CURRENT_LAYER.with(|c| c.get());
if layer.is_none() || !is_enabled() {
return;
}
let step = CURRENT_STEP.with(|c| c.get());
dump(step, layer, op, buf, shape, device);
}
pub fn dump(
step: u64,
layer_idx: Option<usize>,
op: &str,
buf: &MlxBuffer,
shape: &[usize],
device: &MlxDevice,
) {
if !should_dump(layer_idx) {
return;
}
let Some(cfg) = config() else { return };
if let Err(e) = flush_gpu(device) {
eprintln!("[DUMP_BISECT] flush_gpu failed: {e}");
return;
}
if let Err(e) = dump_inner(cfg, step, layer_idx, op, buf, shape) {
eprintln!(
"[DUMP_BISECT] step={step} layer={:?} op={op} dump failed: {e}",
layer_idx
);
}
}
fn flush_gpu(device: &MlxDevice) -> Result<()> {
let active = ACTIVE_SESSION.with(|c| c.get());
if let Some(sess_ptr) = active {
let sess: &mut EncoderSession = unsafe { &mut *sess_ptr };
sess.commit_and_wait()
.context("flush_gpu session.commit_and_wait")?;
sess.reset_for_next_stage()
.context("flush_gpu session.reset_for_next_stage")?;
return Ok(());
}
let mut enc = device.command_encoder().context("flush_gpu enc")?;
enc.commit_and_wait().context("flush_gpu commit_and_wait")?;
Ok(())
}
fn dump_inner(
cfg: &DumpConfig,
step: u64,
layer_idx: Option<usize>,
op: &str,
buf: &MlxBuffer,
shape: &[usize],
) -> Result<()> {
let data = download_f32(buf).context("download_f32 for dump")?;
let layer_str = match layer_idx {
Some(n) => format!("{n:03}"),
None => "___".to_string(),
};
let fname = format!("step{step:04}_layer{layer_str}_{op}.f32");
let path = cfg.dir.join(&fname);
let bytes: &[u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4) };
std::fs::write(&path, bytes).with_context(|| format!("write {}", path.display()))?;
let manifest_path = cfg.dir.join("manifest.txt");
let mut mf = OpenOptions::new()
.create(true)
.append(true)
.open(&manifest_path)
.with_context(|| format!("open manifest {}", manifest_path.display()))?;
let layer_field = match layer_idx {
Some(n) => n.to_string(),
None => "-".to_string(),
};
writeln!(
mf,
"step={step} layer={layer_field} op={op} shape={:?} bytes={} path={}",
shape,
bytes.len(),
fname
)
.ok();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_only_matches_layer_idx() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let f = LayerFilter::Only(5);
match (f, None::<usize>) {
(LayerFilter::Only(_), None) => {}
_ => panic!("always-on ops should match Only filter"),
}
match (f, Some(5)) {
(LayerFilter::Only(n), Some(idx)) if n == idx => {}
_ => panic!("matching layer should pass"),
}
match (f, Some(6)) {
(LayerFilter::Only(n), Some(idx)) if n == idx => panic!("should not match"),
_ => {}
}
}
#[test]
fn filter_all_matches_everything() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let f = LayerFilter::All;
match (f, None::<usize>) {
(LayerFilter::All, _) => {}
_ => panic!("All should match"),
}
match (f, Some(0)) {
(LayerFilter::All, _) => {}
_ => panic!("All should match"),
}
}
#[test]
fn step_counter_monotone() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let a = next_step();
let b = next_step();
assert_eq!(b, a + 1);
}
#[test]
fn synthetic_dump_round_trip() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
use mlx_native::DType;
let device = match MlxDevice::new() {
Ok(d) => d,
Err(_) => return,
};
let mut buf = match device.alloc_buffer(16, DType::F32, vec![4]) {
Ok(b) => b,
Err(_) => return,
};
{
let s = buf.as_mut_slice::<f32>().expect("mut_slice");
s.copy_from_slice(&[1.0_f32, 2.0, 3.0, 4.0]);
}
let tmp_dir = std::env::temp_dir().join(format!(
"hf2q-dump-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&tmp_dir).expect("mkdir tmp");
File::create(tmp_dir.join("manifest.txt")).expect("create manifest");
let cfg = DumpConfig {
filter: LayerFilter::All,
dir: tmp_dir.clone(),
};
dump_inner(&cfg, 0, Some(0), "synthetic", &buf, &[4]).expect("dump_inner round-trip");
let path = tmp_dir.join("step0000_layer000_synthetic.f32");
let bytes = std::fs::read(&path).expect("read dump");
assert_eq!(bytes.len(), 16);
let read: &[f32] = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f32, 4) };
assert_eq!(read, &[1.0, 2.0, 3.0, 4.0]);
let manifest =
std::fs::read_to_string(tmp_dir.join("manifest.txt")).expect("read manifest");
assert!(manifest.contains("step=0"));
assert!(manifest.contains("layer=0"));
assert!(manifest.contains("op=synthetic"));
assert!(manifest.contains("bytes=16"));
std::fs::remove_dir_all(&tmp_dir).ok();
}
}