use std::fs;
use std::io::Write;
use std::path::Path;
use crate::error::InferenceError;
use crate::model::qwen35::qwen_required_tensor_names;
use crate::model::qwen35_config::Qwen35Config;
use crate::quant::quarot::forward_equivalence::{
ForwardEquivalenceConfig, ForwardEquivalenceReport, assert_prepared_forward_equivalence_qwen35,
prepare_forward_equivalence_qwen35_after_admission, validate_forward_equivalence_admission,
};
#[cfg(test)]
use crate::quant::quarot::forward_equivalence::{
pre_admission_allocation_tracking, prepare_forward_equivalence_qwen35,
};
use crate::quant::quarot::hadamard::RandomizedHadamard;
use crate::quant::quarot::io::{ArtifactVersion, OnlineArtifactDescriptor, QuarotTensorReader};
use crate::quant::quarot::lm_head::{
materialize_lm_head_for_qwen35, qwen35_final_norm_fusion_target,
untie_word_embeddings_in_config_json,
};
use crate::quant::quarot::pipeline::{
TensorEntry, absorb_rotations, fuse_rmsnorms, load_tensors_f64,
};
use crate::quant::quarot::plan::RotationPlan;
use crate::quant::quarot::rmsnorm_fusion::qwen35_per_layer_fusion_plan;
use crate::weights::ingress::DecodedTensorValidator;
use crate::weights::q4_weights::{q4_f32_to_finite_f16, quantize_f64_to_q4, save_q4_file};
const MAX_QUAROT_HIDDEN_SIZE: usize = 1 << 20;
#[derive(Debug, Clone)]
pub struct ConversionOptions {
pub rotation_seed: u64,
pub tolerance: f64,
pub num_probe_tokens: usize,
pub dry_run: bool,
}
impl Default for ConversionOptions {
fn default() -> Self {
Self {
rotation_seed: 0xCAFE_BABE_DEAD_BEEF,
tolerance: 1e-5,
num_probe_tokens: 4,
dry_run: false,
}
}
}
#[derive(Debug, Clone)]
pub struct ConversionReport {
pub planned_quantized: usize,
pub kept_f16: usize,
pub total_bytes_in: u64,
pub total_bytes_out: u64,
pub forward_equivalence: ForwardEquivalenceReport,
pub was_tied: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct IndexEntry {
name: String,
file: String,
quantized: bool,
shape: Vec<usize>,
numel: usize,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PromotionState {
Unpromoted,
Promoted,
Rejected,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
pub struct PplGateRecord {
pub unrotated_ppl: f64,
pub quarot_ppl: f64,
pub delta: f64,
pub delta_threshold: f64,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
pub struct PromotionRecord {
pub state: PromotionState,
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ppl_gate: Option<PplGateRecord>,
}
impl PromotionRecord {
fn unpromoted() -> Self {
Self {
state: PromotionState::Unpromoted,
reason: "PPL acceptance gate has not been run against this artifact; run \
`eval_perplexity --q4-dir <unrotated baseline> --quarot-q4-dir <this \
output dir> --tokenizer-dir <src> --corpus-file <corpus>` to record a \
result before treating it as quality-validated (ADR-044 step 4, #1103)."
.to_string(),
ppl_gate: None,
}
}
}
impl Default for PromotionRecord {
fn default() -> Self {
Self {
state: PromotionState::Unpromoted,
reason: "no promotion record present in quantize_index.json (artifact predates \
#1103 promotion tracking, or the gate has not been run)"
.to_string(),
ppl_gate: None,
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct QuantizeIndex {
#[serde(skip_serializing_if = "Option::is_none")]
quarot_seed: Option<u64>,
tensors: Vec<IndexEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
online: Option<OnlineArtifactDescriptor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
artifact_version: Option<ArtifactVersion>,
#[serde(default)]
promotion: PromotionRecord,
}
#[cfg(any(test, feature = "metal-gpu"))]
pub(crate) fn read_quarot_seed_from_index(
q4_dir: &Path,
cfg: &Qwen35Config,
) -> Result<Option<u64>, String> {
let path = q4_dir.join("quantize_index.json");
let Some(bytes) = crate::quant::q4_manifest::read_manifest_bytes_bounded(&path)? else {
return Ok(None);
};
let value: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| format!("{}: malformed quantize_index.json: {e}", path.display()))?;
if value.is_array() {
return Ok(None);
}
let index: QuantizeIndex = serde_json::from_value(value)
.map_err(|e| format!("{}: malformed quantize_index.json: {e}", path.display()))?;
if matches!(index.artifact_version, Some(ArtifactVersion::V1Online)) {
if index.online.is_none() {
return Err(format!(
"{}: artifact_version declares v1-online-r3r4 but the online \
rotation descriptor is missing or null; refusing to load an \
incomplete V1 artifact",
path.display()
));
}
return Err(format!(
"{}: this runtime does not yet execute V1 online rotation \
recipes; artifact requires R3/R4 runtime support",
path.display()
));
}
if let Some(online) = &index.online {
if matches!(online.version, ArtifactVersion::V1Online) {
return Err(format!(
"{}: this runtime does not yet execute V1 online rotation \
recipes; artifact requires R3/R4 runtime support",
path.display()
));
}
online.validate(Some(cfg)).map_err(|e| {
format!(
"{}: invalid online-artifact descriptor: {e}",
path.display()
)
})?;
}
Ok(index.quarot_seed)
}
pub fn record_ppl_gate_result(
quarot_dir: &Path,
unrotated_ppl: f64,
quarot_ppl: f64,
delta_threshold: f64,
) -> Result<PromotionRecord, InferenceError> {
let path = quarot_dir.join("quantize_index.json");
let bytes = fs::read(&path).map_err(|e| {
InferenceError::Inference(format!(
"record_ppl_gate_result: failed to read {}: {e}",
path.display()
))
})?;
let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
InferenceError::Inference(format!(
"record_ppl_gate_result: {}: malformed quantize_index.json: {e}",
path.display()
))
})?;
if value.is_array() {
return Err(InferenceError::Inference(format!(
"record_ppl_gate_result: {} is a bare-array manifest (quantize_q4 shape, no \
promotion field); only a quantize_quarot object-form manifest can record a \
PPL gate result",
path.display()
)));
}
let mut index: QuantizeIndex = serde_json::from_value(value).map_err(|e| {
InferenceError::Inference(format!(
"record_ppl_gate_result: {}: malformed quantize_index.json: {e}",
path.display()
))
})?;
let delta = quarot_ppl - unrotated_ppl;
let passed = delta < delta_threshold;
let record = PromotionRecord {
state: if passed {
PromotionState::Promoted
} else {
PromotionState::Rejected
},
reason: if passed {
format!(
"ADR-044 PPL acceptance gate passed: delta {delta:+.6} < threshold \
{delta_threshold:.6} (quarot {quarot_ppl:.6} - unrotated {unrotated_ppl:.6})"
)
} else {
format!(
"ADR-044 PPL acceptance gate failed: delta {delta:+.6} >= threshold \
{delta_threshold:.6} (quarot {quarot_ppl:.6} - unrotated {unrotated_ppl:.6})"
)
},
ppl_gate: Some(PplGateRecord {
unrotated_ppl,
quarot_ppl,
delta,
delta_threshold,
}),
};
index.promotion = record.clone();
let json = serde_json::to_string_pretty(&index).map_err(|e| {
InferenceError::Inference(format!(
"record_ppl_gate_result: failed to serialize {}: {e}",
path.display()
))
})?;
fs::write(&path, json).map_err(|e| {
InferenceError::Inference(format!(
"record_ppl_gate_result: failed to write {}: {e}",
path.display()
))
})?;
Ok(record)
}
pub fn read_promotion_record(quarot_dir: &Path) -> Result<PromotionRecord, InferenceError> {
let path = quarot_dir.join("quantize_index.json");
let bytes = fs::read(&path).map_err(|e| {
InferenceError::Inference(format!(
"read_promotion_record: failed to read {}: {e}",
path.display()
))
})?;
let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| {
InferenceError::Inference(format!(
"read_promotion_record: {}: malformed quantize_index.json: {e}",
path.display()
))
})?;
if value.is_array() {
return Err(InferenceError::Inference(format!(
"read_promotion_record: {} is a bare-array manifest (quantize_q4 shape); it has \
no promotion field",
path.display()
)));
}
let index: QuantizeIndex = serde_json::from_value(value).map_err(|e| {
InferenceError::Inference(format!(
"read_promotion_record: {}: malformed quantize_index.json: {e}",
path.display()
))
})?;
Ok(index.promotion)
}
fn inject_quarot_seed(json: &str, seed: u64) -> Result<String, InferenceError> {
let mut value: serde_json::Value = serde_json::from_str(json)
.map_err(|e| InferenceError::Inference(format!("inject_quarot_seed: invalid JSON: {e}")))?;
let obj = value.as_object_mut().ok_or_else(|| {
InferenceError::Inference(
"inject_quarot_seed: top-level JSON must be an object".to_string(),
)
})?;
if let Some(text_config) = obj.get_mut("text_config")
&& let Some(text_obj) = text_config.as_object_mut()
{
text_obj.insert(
"quarot_rotation_seed".to_string(),
serde_json::Value::Number(seed.into()),
);
}
obj.insert(
"quarot_rotation_seed".to_string(),
serde_json::Value::Number(seed.into()),
);
serde_json::to_string_pretty(&value).map_err(|e| {
InferenceError::Inference(format!("inject_quarot_seed: serialize failed: {e}"))
})
}
fn f16_file_byte_count(data_len: usize, shape_len: usize) -> u64 {
let header: u64 = 4 + 4 + 4 + 8 * shape_len as u64 + 8;
let payload: u64 = data_len as u64 * 2;
header + payload
}
fn write_mtp_weights_quarot(
reader: &QuarotTensorReader,
source: &str,
output_dir: &Path,
dry_run: bool,
index_entries: &mut Vec<IndexEntry>,
kept_f16: &mut usize,
_planned_quantized: &mut usize,
total_bytes_out: &mut u64,
) -> Result<(), InferenceError> {
let proj_names = [
"mtp.fc.weight",
"mtp.layers.0.self_attn.q_proj.weight",
"mtp.layers.0.self_attn.k_proj.weight",
"mtp.layers.0.self_attn.v_proj.weight",
"mtp.layers.0.self_attn.o_proj.weight",
"mtp.layers.0.mlp.gate_proj.weight",
"mtp.layers.0.mlp.up_proj.weight",
"mtp.layers.0.mlp.down_proj.weight",
];
let norm_names = [
"mtp.layers.0.input_layernorm.weight",
"mtp.layers.0.post_attention_layernorm.weight",
"mtp.layers.0.self_attn.q_norm.weight",
"mtp.layers.0.self_attn.k_norm.weight",
"mtp.norm.weight",
"mtp.pre_fc_norm_embedding.weight",
"mtp.pre_fc_norm_hidden.weight",
];
let mut process_as_f16 = |name: &str| -> Result<(), InferenceError> {
if !reader.has_tensor(name) {
return Ok(());
}
let (data, shape) = reader.read_tensor_f64(name)?;
let sanitized = sanitize_tensor_name(name);
let file_name = format!("{sanitized}.f16");
*total_bytes_out += f16_file_byte_count(data.len(), shape.len());
if !dry_run {
let out_path = output_dir.join(&file_name);
write_f16_file(&out_path, source, name, &data, &shape)?;
}
*kept_f16 += 1;
index_entries.push(IndexEntry {
name: name.to_string(),
file: file_name,
quantized: false,
shape: shape.clone(),
numel: data.len(),
});
Ok(())
};
for name in &proj_names {
process_as_f16(name)?;
}
for name in &norm_names {
process_as_f16(name)?;
}
Ok(())
}
pub fn convert_quarot_qwen35(
input_dir: &Path,
output_dir: &Path,
opts: &ConversionOptions,
) -> Result<ConversionReport, InferenceError> {
if !opts.dry_run {
validate_output_dir_layout(input_dir, output_dir)?;
}
let config_path = input_dir.join("config.json");
let config_json = fs::read_to_string(&config_path).map_err(|e| {
InferenceError::Inference(format!(
"convert_quarot_qwen35: failed to read {}: {e}",
config_path.display()
))
})?;
let cfg = Qwen35Config::from_config_json_str(&config_json)?;
if !cfg.hidden_size.is_power_of_two() {
return Err(InferenceError::Inference(format!(
"convert_quarot_qwen35: hidden_size={} is not a power of 2; \
QuaRot v0 only supports power-of-2 hidden dims \
(see ADR-044 §Model coverage)",
cfg.hidden_size
)));
}
if cfg.hidden_size > MAX_QUAROT_HIDDEN_SIZE {
return Err(InferenceError::Inference(format!(
"convert_quarot_qwen35: hidden_size={} exceeds the maximum supported value \
({MAX_QUAROT_HIDDEN_SIZE}); this is almost certainly a corrupted or hostile \
config.json, not a real model",
cfg.hidden_size
)));
}
if cfg.is_moe() {
return Err(InferenceError::Inference(
"convert_quarot_qwen35: MoE configs are deferred to v1 (see ADR-044 §Out of v0)"
.to_string(),
));
}
let forward_cfg = ForwardEquivalenceConfig {
num_probe_tokens: opts.num_probe_tokens,
tolerance: opts.tolerance,
seed: opts.rotation_seed,
};
#[cfg(test)]
pre_admission_allocation_tracking::mark_converter_boundary();
let forward_admission = validate_forward_equivalence_admission(&cfg, &forward_cfg)?;
#[cfg(test)]
pre_admission_allocation_tracking::mark_reader_boundary();
let reader = QuarotTensorReader::open(input_dir)?;
let input_source = input_dir.display().to_string();
let required_names = qwen_required_tensor_names(&cfg);
let mut total_bytes_in: u64 = required_names
.iter()
.map(|name| reader.tensor_byte_len(name))
.collect::<Result<Vec<u64>, _>>()?
.into_iter()
.sum();
if cfg.mtp_num_hidden_layers > 0 {
let mtp_names = [
"mtp.fc.weight",
"mtp.layers.0.self_attn.q_proj.weight",
"mtp.layers.0.self_attn.k_proj.weight",
"mtp.layers.0.self_attn.v_proj.weight",
"mtp.layers.0.self_attn.o_proj.weight",
"mtp.layers.0.mlp.gate_proj.weight",
"mtp.layers.0.mlp.up_proj.weight",
"mtp.layers.0.mlp.down_proj.weight",
"mtp.layers.0.input_layernorm.weight",
"mtp.layers.0.post_attention_layernorm.weight",
"mtp.layers.0.self_attn.q_norm.weight",
"mtp.layers.0.self_attn.k_norm.weight",
"mtp.norm.weight",
"mtp.pre_fc_norm_embedding.weight",
"mtp.pre_fc_norm_hidden.weight",
];
for name in &mtp_names {
if reader.has_tensor(name) {
total_bytes_in += reader.tensor_byte_len(name)?;
}
}
}
let mut working_set = load_tensors_f64(&reader, &required_names)?;
let was_tied = cfg.tie_word_embeddings;
if was_tied {
working_set.reserve(1);
}
#[cfg(test)]
pre_admission_allocation_tracking::mark_materialized_working_set_boundary();
if was_tied {
materialize_lm_head_for_qwen35(&mut working_set, &cfg)?;
}
let rotation = RandomizedHadamard::new(opts.rotation_seed, cfg.hidden_size)?;
let equivalence_snapshot = prepare_forward_equivalence_qwen35_after_admission(
&working_set,
&rotation,
forward_admission,
)?;
#[cfg(test)]
pre_admission_allocation_tracking::mark_materialized_working_set_boundary_completed();
let mut fusion_plan = qwen35_per_layer_fusion_plan(&cfg)?;
fusion_plan.push(qwen35_final_norm_fusion_target());
let rotation_plan = RotationPlan::qwen35_residual_stream_linear_layers();
fuse_rmsnorms(&mut working_set, &fusion_plan)?;
absorb_rotations(&mut working_set, &rotation_plan, &rotation)?;
let forward_equivalence =
assert_prepared_forward_equivalence_qwen35(equivalence_snapshot, &reader, &working_set)?;
if !opts.dry_run {
fs::create_dir_all(output_dir).map_err(|e| {
InferenceError::Inference(format!(
"convert_quarot_qwen35: failed to create output directory {}: {e}",
output_dir.display()
))
})?;
}
let mut names: Vec<String> = working_set.keys().cloned().collect();
names.sort();
let mut index_entries: Vec<IndexEntry> = Vec::with_capacity(names.len());
let mut planned_quantized: usize = 0;
let mut kept_f16: usize = 0;
let mut total_bytes_out: u64 = 0;
for name in &names {
let entry: &TensorEntry = &working_set[name];
let sanitized = sanitize_tensor_name(name);
let is_planned = rotation_plan.for_tensor(name).is_some();
if is_planned {
if entry.shape.len() != 2 {
return Err(InferenceError::Inference(format!(
"convert_quarot_qwen35: planned tensor `{name}` has shape {:?}, \
expected 2-D for Q4 quantization (rotation plan invariant violated)",
entry.shape
)));
}
let header_bytes = (4 + 4 + 4 + 8 * entry.shape.len() + 8) as u64;
let n_blocks = entry.data.len().div_ceil(32) as u64;
total_bytes_out += header_bytes + n_blocks.saturating_mul(20);
if !opts.dry_run {
let q4 = quantize_f64_to_q4(&entry.data, &entry.shape)?;
let file_name = format!("{sanitized}.q4");
let out_path = output_dir.join(&file_name);
save_q4_file(&out_path, &q4).map_err(|e| {
InferenceError::Inference(format!(
"convert_quarot_qwen35: failed to write {}: {e}",
out_path.display()
))
})?;
index_entries.push(IndexEntry {
name: name.clone(),
file: file_name,
quantized: true,
shape: entry.shape.clone(),
numel: entry.data.len(),
});
}
planned_quantized += 1;
} else {
total_bytes_out += f16_file_byte_count(entry.data.len(), entry.shape.len());
if !opts.dry_run {
let file_name = format!("{sanitized}.f16");
let out_path = output_dir.join(&file_name);
write_f16_file(&out_path, &input_source, name, &entry.data, &entry.shape)?;
index_entries.push(IndexEntry {
name: name.clone(),
file: file_name,
quantized: false,
shape: entry.shape.clone(),
numel: entry.data.len(),
});
}
kept_f16 += 1;
}
}
if cfg.mtp_num_hidden_layers > 0 {
write_mtp_weights_quarot(
&reader,
&input_source,
output_dir,
opts.dry_run,
&mut index_entries,
&mut kept_f16,
&mut planned_quantized,
&mut total_bytes_out,
)?;
}
if !opts.dry_run {
let index_path = output_dir.join("quantize_index.json");
let index_record = QuantizeIndex {
quarot_seed: Some(opts.rotation_seed),
tensors: index_entries,
online: None,
artifact_version: None,
promotion: PromotionRecord::unpromoted(),
};
let index_json = serde_json::to_string_pretty(&index_record).map_err(|e| {
InferenceError::Inference(format!(
"convert_quarot_qwen35: failed to serialize quantize_index.json: {e}"
))
})?;
fs::write(&index_path, index_json).map_err(|e| {
InferenceError::Inference(format!(
"convert_quarot_qwen35: failed to write {}: {e}",
index_path.display()
))
})?;
let mut output_config_json = untie_word_embeddings_in_config_json(&config_json)?;
output_config_json = inject_quarot_seed(&output_config_json, opts.rotation_seed)?;
let out_config_path = output_dir.join("config.json");
fs::write(&out_config_path, &output_config_json).map_err(|e| {
InferenceError::Inference(format!(
"convert_quarot_qwen35: failed to write {}: {e}",
out_config_path.display()
))
})?;
}
Ok(ConversionReport {
planned_quantized,
kept_f16,
total_bytes_in,
total_bytes_out,
forward_equivalence,
was_tied,
})
}
fn validate_output_dir_layout(input_dir: &Path, output_dir: &Path) -> Result<(), InferenceError> {
let input_canon = fs::canonicalize(input_dir).map_err(|e| {
InferenceError::Inference(format!(
"validate_output_dir_layout: cannot canonicalize input_dir {}: {e}",
input_dir.display()
))
})?;
if !output_dir.exists() {
return Ok(());
}
let output_canon = fs::canonicalize(output_dir).map_err(|e| {
InferenceError::Inference(format!(
"validate_output_dir_layout: cannot canonicalize output_dir {}: {e}",
output_dir.display()
))
})?;
if input_canon == output_canon {
return Err(InferenceError::Inference(format!(
"validate_output_dir_layout: input and output directories resolve to the same \
path ({}); refusing to overwrite source artifacts. Pass a separate \
--output-dir to avoid corrupting the input checkpoint.",
input_canon.display()
)));
}
let mut entries = fs::read_dir(output_dir).map_err(|e| {
InferenceError::Inference(format!(
"validate_output_dir_layout: cannot read output_dir {}: {e}",
output_dir.display()
))
})?;
if entries.next().is_some() {
return Err(InferenceError::Inference(format!(
"validate_output_dir_layout: output_dir {} is not empty; refusing to mix \
new conversion output with pre-existing files. Remove the directory or \
pass a fresh path — a refused conversion must not leave a partial mix \
of stale + new artifacts.",
output_canon.display()
)));
}
Ok(())
}
fn sanitize_tensor_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' {
c
} else {
'_'
}
})
.collect()
}
fn write_f16_file(
path: &Path,
source: &str,
tensor_name: &str,
data: &[f64],
shape: &[usize],
) -> Result<usize, InferenceError> {
let mut validator =
DecodedTensorValidator::decoded_input(source, tensor_name, shape, "F16", data.len())?;
let mut payload = Vec::with_capacity(data.len() * 2);
for &value in data {
let bits =
q4_f32_to_finite_f16(value as f32).map_err(|bits| validator.reject_f16_bits(bits))?;
validator.observe_finite();
payload.extend_from_slice(&bits.to_le_bytes());
}
validator.finish()?;
let mut file = fs::File::create(path).map_err(|e| {
InferenceError::Inference(format!(
"write_f16_file: failed to create {}: {e}",
path.display()
))
})?;
let mut bytes_written: usize = 0;
let mut write_all = |buf: &[u8]| -> Result<(), InferenceError> {
file.write_all(buf).map_err(|e| {
InferenceError::Inference(format!(
"write_f16_file: write failure on {}: {e}",
path.display()
))
})
};
write_all(b"KHF1")?;
bytes_written += 4;
write_all(&1u32.to_le_bytes())?;
bytes_written += 4;
write_all(&(shape.len() as u32).to_le_bytes())?;
bytes_written += 4;
for &dim in shape {
write_all(&(dim as u64).to_le_bytes())?;
bytes_written += 8;
}
write_all(&(data.len() as u64).to_le_bytes())?;
bytes_written += 8;
write_all(&payload)?;
bytes_written += payload.len();
Ok(bytes_written)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::qwen35_config::{LayerType, compute_layer_types};
use crate::quant::quarot::lm_head::{
QWEN35_EMBED_TOKENS_NAME, QWEN35_FINAL_NORM_NAME, QWEN35_LM_HEAD_NAME,
};
use crate::weights::q4_weights::q4_f32_to_f16;
use serde_json::Value;
use std::path::PathBuf;
fn tiny_cfg(tied: bool) -> Qwen35Config {
let mut cfg = Qwen35Config::qwen35_0_8b();
cfg.hidden_size = 8;
cfg.num_hidden_layers = 2;
cfg.vocab_size = 4;
cfg.intermediate_size = 16;
cfg.num_attention_heads = 2;
cfg.num_key_value_heads = 1;
cfg.head_dim = 4;
cfg.linear_num_key_heads = 1;
cfg.linear_key_head_dim = 2;
cfg.linear_value_head_dim = 2;
cfg.linear_num_value_heads = Some(1);
cfg.linear_conv_kernel_dim = 4;
cfg.full_attention_interval = 2;
cfg.layer_types = compute_layer_types(cfg.num_hidden_layers, cfg.full_attention_interval);
cfg.layer_mask = vec![true; cfg.num_hidden_layers];
cfg.tie_word_embeddings = tied;
cfg.rms_norm_eps = 1e-6;
cfg.partial_rotary_factor = 0.5;
cfg.rope_theta = 1_000_000.0;
cfg.max_position_embeddings = 1024;
cfg.eos_token_id = 3;
cfg
}
fn tiny_config_json(cfg: &Qwen35Config) -> String {
let layer_types: Vec<Value> = cfg
.layer_types
.iter()
.map(|t| match t {
LayerType::FullAttention => Value::String("full_attention".into()),
LayerType::LinearAttention => Value::String("linear_attention".into()),
})
.collect();
let mut text_config = serde_json::Map::new();
text_config.insert("hidden_size".into(), Value::from(cfg.hidden_size));
text_config.insert(
"num_hidden_layers".into(),
Value::from(cfg.num_hidden_layers),
);
text_config.insert("vocab_size".into(), Value::from(cfg.vocab_size));
text_config.insert(
"intermediate_size".into(),
Value::from(cfg.intermediate_size),
);
text_config.insert("rms_norm_eps".into(), Value::from(cfg.rms_norm_eps));
text_config.insert(
"num_attention_heads".into(),
Value::from(cfg.num_attention_heads),
);
text_config.insert(
"num_key_value_heads".into(),
Value::from(cfg.num_key_value_heads),
);
text_config.insert("head_dim".into(), Value::from(cfg.head_dim));
text_config.insert("rope_theta".into(), Value::from(cfg.rope_theta));
text_config.insert(
"partial_rotary_factor".into(),
Value::from(cfg.partial_rotary_factor),
);
text_config.insert(
"linear_num_key_heads".into(),
Value::from(cfg.linear_num_key_heads),
);
if let Some(v) = cfg.linear_num_value_heads {
text_config.insert("linear_num_value_heads".into(), Value::from(v));
}
text_config.insert(
"linear_key_head_dim".into(),
Value::from(cfg.linear_key_head_dim),
);
text_config.insert(
"linear_value_head_dim".into(),
Value::from(cfg.linear_value_head_dim),
);
text_config.insert(
"linear_conv_kernel_dim".into(),
Value::from(cfg.linear_conv_kernel_dim),
);
text_config.insert(
"tie_word_embeddings".into(),
Value::from(cfg.tie_word_embeddings),
);
text_config.insert(
"full_attention_interval".into(),
Value::from(cfg.full_attention_interval),
);
text_config.insert("layer_types".into(), Value::Array(layer_types));
text_config.insert("eos_token_id".into(), Value::from(cfg.eos_token_id));
text_config.insert(
"max_position_embeddings".into(),
Value::from(cfg.max_position_embeddings),
);
if let Some(v) = cfg.num_experts {
text_config.insert("num_experts".into(), Value::from(v));
}
if let Some(v) = cfg.num_experts_per_tok {
text_config.insert("num_experts_per_tok".into(), Value::from(v));
}
if let Some(v) = cfg.moe_intermediate_size {
text_config.insert("moe_intermediate_size".into(), Value::from(v));
}
if let Some(v) = cfg.shared_expert_intermediate_size {
text_config.insert("shared_expert_intermediate_size".into(), Value::from(v));
}
serde_json::to_string_pretty(&serde_json::json!({
"tie_word_embeddings": cfg.tie_word_embeddings,
"text_config": Value::Object(text_config),
}))
.unwrap()
}
fn f32_to_bf16_bits(v: f32) -> u16 {
let bits = v.to_bits();
let lsb = (bits >> 16) & 1;
let rounding_bias = 0x7fff + lsb;
((bits.wrapping_add(rounding_bias)) >> 16) as u16
}
fn write_test_safetensors(path: &Path, tensors: &[(&str, Vec<usize>, &[f64])]) {
let mut header = serde_json::Map::new();
let mut payload: Vec<u8> = Vec::new();
for (name, shape, values) in tensors {
assert_eq!(values.len(), shape.iter().product::<usize>());
let start = payload.len();
for &v in *values {
payload.extend_from_slice(&(v as f32).to_le_bytes());
}
let end = payload.len();
let mut entry = serde_json::Map::new();
entry.insert("dtype".into(), Value::String("F32".into()));
entry.insert(
"shape".into(),
Value::Array(shape.iter().map(|d| Value::from(*d as u64)).collect()),
);
entry.insert(
"data_offsets".into(),
Value::Array(vec![Value::from(start as u64), Value::from(end as u64)]),
);
header.insert((*name).to_string(), Value::Object(entry));
}
let header_str = serde_json::to_string(&Value::Object(header)).unwrap();
let mut file = fs::File::create(path).unwrap();
file.write_all(&(header_str.len() as u64).to_le_bytes())
.unwrap();
file.write_all(header_str.as_bytes()).unwrap();
file.write_all(&payload).unwrap();
}
fn synth_data(n: usize, seed: u64) -> Vec<f64> {
let mut state = seed;
(0..n)
.map(|_| {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = (state >> 11) as u32;
(bits as f64 / u32::MAX as f64) - 0.5
})
.collect()
}
const TIED_LM_HEAD_PERTURBED_INDEX: usize = 0;
const TIED_LM_HEAD_PERTURBATION: f64 = 1.0 / 32_768.0;
fn write_required_tensors_for(
cfg: &Qwen35Config,
path: &Path,
seed: u64,
include_tied_lm_head: bool,
) {
let hidden = cfg.hidden_size;
let vocab = cfg.vocab_size;
let intermediate = cfg.intermediate_size;
let head_dim = cfg.head_dim;
let full_q_dim = cfg.full_q_dim();
let full_kv_dim = cfg.full_kv_dim();
let linear_qkv_dim = cfg.linear_qkv_dim();
let linear_output_dim = cfg.linear_output_dim();
let linear_num_heads = cfg.linear_num_key_heads;
let kernel = cfg.linear_conv_kernel_dim;
let mut entries: Vec<(String, Vec<usize>, Vec<f64>)> = Vec::new();
let mut s = seed;
let mut next = |n: usize| -> Vec<f64> {
s = s.wrapping_add(1);
synth_data(n, s)
};
let mut embed_tokens = next(vocab * hidden);
let mut final_norm = next(hidden);
let tied_lm_head = if cfg.tie_word_embeddings && include_tied_lm_head {
embed_tokens[TIED_LM_HEAD_PERTURBED_INDEX] = 0.25;
final_norm[TIED_LM_HEAD_PERTURBED_INDEX] = 0.0;
let mut lm_head = embed_tokens.clone();
lm_head[TIED_LM_HEAD_PERTURBED_INDEX] += TIED_LM_HEAD_PERTURBATION;
Some(lm_head)
} else {
None
};
entries.push((
"model.language_model.embed_tokens.weight".to_string(),
vec![vocab, hidden],
embed_tokens,
));
entries.push((
"model.language_model.norm.weight".to_string(),
vec![hidden],
final_norm,
));
if let Some(lm_head) = tied_lm_head {
entries.push(("lm_head.weight".to_string(), vec![vocab, hidden], lm_head));
} else if !cfg.tie_word_embeddings {
entries.push((
"lm_head.weight".to_string(),
vec![vocab, hidden],
next(vocab * hidden),
));
}
for i in 0..cfg.num_hidden_layers {
let prefix = format!("model.language_model.layers.{i}");
entries.push((
format!("{prefix}.input_layernorm.weight"),
vec![hidden],
next(hidden),
));
entries.push((
format!("{prefix}.post_attention_layernorm.weight"),
vec![hidden],
next(hidden),
));
if cfg.is_full_attention(i) {
entries.push((
format!("{prefix}.self_attn.q_proj.weight"),
vec![2 * full_q_dim, hidden],
next(2 * full_q_dim * hidden),
));
entries.push((
format!("{prefix}.self_attn.k_proj.weight"),
vec![full_kv_dim, hidden],
next(full_kv_dim * hidden),
));
entries.push((
format!("{prefix}.self_attn.v_proj.weight"),
vec![full_kv_dim, hidden],
next(full_kv_dim * hidden),
));
entries.push((
format!("{prefix}.self_attn.o_proj.weight"),
vec![hidden, full_q_dim],
next(hidden * full_q_dim),
));
entries.push((
format!("{prefix}.self_attn.q_norm.weight"),
vec![head_dim],
next(head_dim),
));
entries.push((
format!("{prefix}.self_attn.k_norm.weight"),
vec![head_dim],
next(head_dim),
));
} else {
entries.push((
format!("{prefix}.linear_attn.in_proj_qkv.weight"),
vec![linear_qkv_dim, hidden],
next(linear_qkv_dim * hidden),
));
entries.push((
format!("{prefix}.linear_attn.in_proj_z.weight"),
vec![linear_output_dim, hidden],
next(linear_output_dim * hidden),
));
entries.push((
format!("{prefix}.linear_attn.in_proj_b.weight"),
vec![linear_num_heads, hidden],
next(linear_num_heads * hidden),
));
entries.push((
format!("{prefix}.linear_attn.in_proj_a.weight"),
vec![linear_num_heads, hidden],
next(linear_num_heads * hidden),
));
entries.push((
format!("{prefix}.linear_attn.A_log"),
vec![linear_num_heads],
next(linear_num_heads),
));
entries.push((
format!("{prefix}.linear_attn.dt_bias"),
vec![linear_num_heads],
next(linear_num_heads),
));
entries.push((
format!("{prefix}.linear_attn.conv1d.weight"),
vec![linear_qkv_dim, 1, kernel],
next(linear_qkv_dim * kernel),
));
entries.push((
format!("{prefix}.linear_attn.norm.weight"),
vec![linear_output_dim],
next(linear_output_dim),
));
entries.push((
format!("{prefix}.linear_attn.out_proj.weight"),
vec![hidden, linear_output_dim],
next(hidden * linear_output_dim),
));
}
entries.push((
format!("{prefix}.mlp.gate_proj.weight"),
vec![intermediate, hidden],
next(intermediate * hidden),
));
entries.push((
format!("{prefix}.mlp.up_proj.weight"),
vec![intermediate, hidden],
next(intermediate * hidden),
));
entries.push((
format!("{prefix}.mlp.down_proj.weight"),
vec![hidden, intermediate],
next(hidden * intermediate),
));
}
let borrowed: Vec<(&str, Vec<usize>, &[f64])> = entries
.iter()
.map(|(n, s, d)| (n.as_str(), s.clone(), d.as_slice()))
.collect();
write_test_safetensors(path, &borrowed);
}
fn write_input_dir(cfg: &Qwen35Config, dir: &Path, seed: u64) {
fs::create_dir_all(dir).unwrap();
fs::write(dir.join("config.json"), tiny_config_json(cfg)).unwrap();
write_required_tensors_for(cfg, &dir.join("model.safetensors"), seed, false);
}
#[test]
fn convert_quarot_qwen35_tied_end_to_end() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 1);
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xC0FFEE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
assert!(report.was_tied);
assert!(report.planned_quantized > 0);
assert!(report.kept_f16 > 0);
assert!(report.total_bytes_out > 0);
assert!(report.forward_equivalence.max_abs_error <= 1e-5);
assert!(output.join("config.json").exists());
assert!(output.join("quantize_index.json").exists());
let lm_head_q4 = output.join("lm_head_weight.q4");
assert!(
lm_head_q4.exists(),
"lm_head .q4 should exist: {lm_head_q4:?}"
);
let out_cfg_str = fs::read_to_string(output.join("config.json")).unwrap();
let out_cfg = Qwen35Config::from_config_json_str(&out_cfg_str).unwrap();
assert!(
!out_cfg.tie_word_embeddings,
"output config must be untied after tied-input conversion"
);
let idx_str = fs::read_to_string(output.join("quantize_index.json")).unwrap();
let idx: serde_json::Value = serde_json::from_str(&idx_str).unwrap();
let tensors = idx
.get("tensors")
.and_then(|v| v.as_array())
.expect("quantize_index.json must have a `tensors` array");
assert_eq!(tensors.len(), report.planned_quantized + report.kept_f16);
assert!(
idx.get("quarot_seed")
.and_then(serde_json::Value::as_u64)
.is_some(),
"quantize_index.json must carry quarot_seed (ADR-051 contract)"
);
}
#[test]
fn convert_quarot_qwen35_untied_end_to_end() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(false);
write_input_dir(&cfg, &input, 2);
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xFEED_FACE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
assert!(!report.was_tied);
assert!(report.planned_quantized > 0);
assert!(output.join("config.json").exists());
let out_cfg_str = fs::read_to_string(output.join("config.json")).unwrap();
let out_cfg = Qwen35Config::from_config_json_str(&out_cfg_str).unwrap();
assert!(!out_cfg.tie_word_embeddings);
}
#[test]
fn converter_rejects_probe_budget_before_tensor_materialization() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json(&cfg)).unwrap();
let tracking = pre_admission_allocation_tracking::start_at_converter_boundary();
let result = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
num_probe_tokens: 2_000_000,
dry_run: true,
..Default::default()
},
);
let observation = tracking.finish();
assert!(
observation.rejection_seen,
"the converter call must reach budget rejection"
);
assert_eq!(
observation.before_rejection_allocation_calls, 0,
"the converter allocated between config preflight and budget rejection"
);
assert!(
observation.after_rejection_allocation_calls > 0,
"the diagnostic allocation after budget rejection must be observed"
);
let error = result
.expect_err("the over-budget conversion must fail admission")
.to_string();
assert!(
error.contains("retained chain-logit budget"),
"unexpected error: {error}"
);
}
fn assert_config_rejected_before_tensor_materialization(
opts: ConversionOptions,
expected_error: &str,
) {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 0x0A11_CE55);
let tracking = pre_admission_allocation_tracking::start_at_converter_boundary();
let result = convert_quarot_qwen35(&input, &output, &opts);
let observation = tracking.finish();
assert!(
observation.rejection_seen,
"the converter call must reach config admission rejection"
);
assert_eq!(
observation.before_rejection_allocation_calls, 0,
"config admission allocated before rejection"
);
assert!(
observation.after_rejection_allocation_calls > 0,
"the diagnostic allocation after config rejection must be observed"
);
assert!(
!observation.reader_boundary_seen,
"config admission rejection must precede reader access"
);
assert!(
!observation.materialized_working_set_boundary_seen,
"config admission rejection must precede tensor materialization"
);
let error = result
.expect_err("invalid forward-equivalence config must fail admission")
.to_string();
assert!(error.contains(expected_error), "unexpected error: {error}");
}
#[test]
fn converter_rejects_zero_probe_count_before_tensor_materialization() {
assert_config_rejected_before_tensor_materialization(
ConversionOptions {
num_probe_tokens: 0,
dry_run: true,
..Default::default()
},
"num_probe_tokens must be > 0",
);
}
#[test]
fn converter_rejects_zero_tolerance_before_tensor_materialization() {
assert_config_rejected_before_tensor_materialization(
ConversionOptions {
tolerance: 0.0,
dry_run: true,
..Default::default()
},
"tolerance must be a positive finite value",
);
}
#[test]
fn converter_rejects_nan_tolerance_before_tensor_materialization() {
assert_config_rejected_before_tensor_materialization(
ConversionOptions {
tolerance: f64::NAN,
dry_run: true,
..Default::default()
},
"tolerance must be a positive finite value",
);
}
#[test]
fn converter_does_not_clone_materialized_working_set() {
const REQUIRED_MATERIALIZATION_AND_PREPARE_ALLOCATION_CALLS: usize = 152;
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 0xA110_CA7E);
let tracking =
pre_admission_allocation_tracking::start_at_materialized_working_set_boundary();
convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
num_probe_tokens: 2,
dry_run: true,
..Default::default()
},
)
.unwrap();
let observation = tracking.finish();
assert!(
observation.materialized_working_set_boundary_seen,
"the converter must reach the materialized working-set boundary"
);
assert!(
observation.materialized_working_set_boundary_completed,
"the converter must close the materialized working-set boundary"
);
assert_eq!(
observation.materialized_working_set_allocation_calls,
REQUIRED_MATERIALIZATION_AND_PREPARE_ALLOCATION_CALLS,
"tied-head materialization and forward-equivalence preparation performed \
unexpected owned allocations (a reintroduced working-set clone would show up here)"
);
}
#[test]
fn prepared_equivalence_streams_original_tensors_and_refuses_corruption() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let cfg = tiny_cfg(true);
fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json(&cfg)).unwrap();
write_required_tensors_for(&cfg, &input.join("model.safetensors"), 0x1074, true);
let reader = QuarotTensorReader::open(&input).unwrap();
assert!(
reader.has_tensor(QWEN35_LM_HEAD_NAME),
"the tied fixture must contain a competing on-disk lm_head"
);
let (disk_embed, _) = reader.read_tensor_f64(QWEN35_EMBED_TOKENS_NAME).unwrap();
let (disk_lm_head, _) = reader.read_tensor_f64(QWEN35_LM_HEAD_NAME).unwrap();
let (disk_final_norm, _) = reader.read_tensor_f64(QWEN35_FINAL_NORM_NAME).unwrap();
let differing_indices = disk_lm_head
.iter()
.zip(&disk_embed)
.enumerate()
.filter_map(|(index, (lm_head, embed))| (lm_head != embed).then_some(index))
.collect::<Vec<_>>();
assert_eq!(
differing_indices,
vec![TIED_LM_HEAD_PERTURBED_INDEX],
"the competing lm_head must differ at exactly one element"
);
let source_delta = (disk_lm_head[TIED_LM_HEAD_PERTURBED_INDEX]
- disk_embed[TIED_LM_HEAD_PERTURBED_INDEX])
.abs();
assert_eq!(source_delta, TIED_LM_HEAD_PERTURBATION);
let tolerance = 1e-5;
let transformed_delta = source_delta
* (1.0 + disk_final_norm[TIED_LM_HEAD_PERTURBED_INDEX]).abs()
/ (cfg.hidden_size as f64).sqrt();
assert!(
transformed_delta > tolerance && transformed_delta < 1.1 * tolerance,
"controlled post-fusion/rotation delta {transformed_delta} must sit just above \
tolerance {tolerance}"
);
let required_names = qwen_required_tensor_names(&cfg);
let mut working_set = load_tensors_f64(&reader, &required_names).unwrap();
materialize_lm_head_for_qwen35(&mut working_set, &cfg).unwrap();
let rotation = RandomizedHadamard::new(0xA11C_E5E5, cfg.hidden_size).unwrap();
let forward_cfg = ForwardEquivalenceConfig {
num_probe_tokens: 2,
tolerance,
..Default::default()
};
let passing_snapshot =
prepare_forward_equivalence_qwen35(&working_set, &cfg, &rotation, &forward_cfg)
.unwrap();
let refusing_snapshot =
prepare_forward_equivalence_qwen35(&working_set, &cfg, &rotation, &forward_cfg)
.unwrap();
let mut fusion_plan = qwen35_per_layer_fusion_plan(&cfg).unwrap();
fusion_plan.push(qwen35_final_norm_fusion_target());
let rotation_plan = RotationPlan::qwen35_residual_stream_linear_layers();
fuse_rmsnorms(&mut working_set, &fusion_plan).unwrap();
absorb_rotations(&mut working_set, &rotation_plan, &rotation).unwrap();
let report =
assert_prepared_forward_equivalence_qwen35(passing_snapshot, &reader, &working_set)
.unwrap();
assert!(report.max_abs_error <= forward_cfg.tolerance);
let chain_skipped = "model.language_model.layers.1.self_attn.k_proj.weight";
working_set
.get_mut(chain_skipped)
.expect("full-attention k_proj must exist")
.data[0] += 0.25;
let err =
assert_prepared_forward_equivalence_qwen35(refusing_snapshot, &reader, &working_set)
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("per-tensor"), "unexpected error: {msg}");
}
#[test]
fn convert_quarot_qwen35_dry_run_writes_nothing() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 3);
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xDEADBEEF,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: true,
},
)
.unwrap();
assert!(
report.planned_quantized > 0,
"dry-run must report planned_quantized > 0"
);
assert!(report.kept_f16 > 0, "dry-run must report kept_f16 > 0");
assert!(
report.total_bytes_out > 0,
"dry-run must report total_bytes_out > 0"
);
assert!(report.forward_equivalence.max_abs_error <= 1e-5);
assert!(
!output.exists(),
"dry-run must not create the output directory"
);
}
#[test]
fn convert_quarot_qwen35_refuses_when_tolerance_unmet() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 4);
let err = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xAB12_34CD,
tolerance: 0.0_f64.next_up(), num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("forward-equivalence refused") || msg.contains("exceeds tolerance"),
"unexpected error: {msg}"
);
assert!(
!output.exists(),
"refused conversion must not create the output directory"
);
}
#[test]
fn convert_quarot_qwen35_errors_when_config_missing() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
fs::create_dir_all(&input).unwrap();
let err =
convert_quarot_qwen35(&input, &output, &ConversionOptions::default()).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("config.json"), "unexpected error: {msg}");
}
#[test]
fn convert_quarot_qwen35_rejects_non_power_of_two_hidden() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let mut cfg = tiny_cfg(true);
cfg.hidden_size = 10; fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json(&cfg)).unwrap();
let err =
convert_quarot_qwen35(&input, &output, &ConversionOptions::default()).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("hidden_size=10") && msg.contains("power of 2"),
"unexpected error: {msg}"
);
assert!(!output.exists());
}
#[test]
fn convert_quarot_qwen35_rejects_hostile_hidden_size() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let mut cfg = tiny_cfg(true);
cfg.hidden_size = 1 << 60;
fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json(&cfg)).unwrap();
let err =
convert_quarot_qwen35(&input, &output, &ConversionOptions::default()).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("hidden_size") && msg.contains("exceeds"),
"unexpected error: {msg}"
);
assert!(!output.exists());
}
#[test]
fn convert_quarot_qwen35_rejects_moe_config() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let mut moe_cfg = tiny_cfg(true);
moe_cfg.num_experts = Some(2);
moe_cfg.num_experts_per_tok = Some(1);
moe_cfg.moe_intermediate_size = Some(moe_cfg.intermediate_size);
fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json(&moe_cfg)).unwrap();
let err =
convert_quarot_qwen35(&input, &output, &ConversionOptions::default()).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("MoE"), "unexpected error: {msg}");
assert!(
!output.exists(),
"MoE-rejected conversion must not create output dir"
);
}
#[test]
fn dry_run_bytes_out_matches_real_write() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output_dry = tmp.path().join("output_dry");
let output_real = tmp.path().join("output_real");
let cfg = tiny_cfg(false); write_input_dir(&cfg, &input, 99);
let opts = ConversionOptions {
rotation_seed: 0xABCD_5678,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
};
let dry_report = convert_quarot_qwen35(
&input,
&output_dry,
&ConversionOptions {
dry_run: true,
..opts.clone()
},
)
.unwrap();
let real_report = convert_quarot_qwen35(&input, &output_real, &opts).unwrap();
assert_eq!(
dry_report.total_bytes_out, real_report.total_bytes_out,
"dry-run total_bytes_out ({}) must equal real-write total_bytes_out ({})",
dry_report.total_bytes_out, real_report.total_bytes_out,
);
assert!(
dry_report.total_bytes_out > 0,
"total_bytes_out must be > 0; got 0 (accounting is broken)"
);
assert_eq!(
dry_report.planned_quantized, real_report.planned_quantized,
"planned_quantized mismatch between dry and real"
);
assert_eq!(
dry_report.kept_f16, real_report.kept_f16,
"kept_f16 mismatch between dry and real"
);
assert!(
!output_dry.exists(),
"dry-run must not create the output directory"
);
let mut on_disk: u64 = 0;
for dent in std::fs::read_dir(&output_real).unwrap() {
let path = dent.unwrap().path();
if matches!(
path.extension().and_then(|e| e.to_str()),
Some("q4") | Some("f16")
) {
on_disk += std::fs::metadata(&path).unwrap().len();
}
}
assert_eq!(
real_report.total_bytes_out, on_disk,
"reported total_bytes_out ({}) must equal summed on-disk .q4/.f16 file sizes ({})",
real_report.total_bytes_out, on_disk,
);
}
#[test]
fn dry_run_bytes_out_matches_real_write_tied() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output_dry = tmp.path().join("output_dry");
let output_real = tmp.path().join("output_real");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 100);
let opts = ConversionOptions {
rotation_seed: 0xFACE_CAFE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
};
let dry_report = convert_quarot_qwen35(
&input,
&output_dry,
&ConversionOptions {
dry_run: true,
..opts.clone()
},
)
.unwrap();
let real_report = convert_quarot_qwen35(&input, &output_real, &opts).unwrap();
assert_eq!(
dry_report.total_bytes_out, real_report.total_bytes_out,
"tied: dry-run total_bytes_out ({}) must equal real-write total_bytes_out ({})",
dry_report.total_bytes_out, real_report.total_bytes_out,
);
assert!(dry_report.total_bytes_out > 0);
assert!(
!output_dry.exists(),
"dry-run must not create the output directory"
);
}
#[test]
fn sanitize_tensor_name_replaces_dots_and_slashes() {
assert_eq!(
sanitize_tensor_name("model.layers.0.mlp.gate_proj.weight"),
"model_layers_0_mlp_gate_proj_weight"
);
assert_eq!(sanitize_tensor_name("lm_head.weight"), "lm_head_weight");
assert_eq!(sanitize_tensor_name("a/b\\c"), "a_b_c");
}
#[test]
fn f16_file_has_khf1_header_and_correct_size() {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("test.f16");
let data = vec![1.5_f64, -2.5, 0.25, -0.125];
let shape = vec![2_usize, 2];
let bytes_written =
write_f16_file(&p, "fixture.safetensors", "fixture.weight", &data, &shape).unwrap();
let raw = fs::read(&p).unwrap();
assert_eq!(&raw[0..4], b"KHF1");
assert_eq!(u32::from_le_bytes(raw[4..8].try_into().unwrap()), 1);
assert_eq!(u32::from_le_bytes(raw[8..12].try_into().unwrap()), 2);
assert_eq!(u64::from_le_bytes(raw[12..20].try_into().unwrap()), 2);
assert_eq!(u64::from_le_bytes(raw[20..28].try_into().unwrap()), 2);
assert_eq!(u64::from_le_bytes(raw[28..36].try_into().unwrap()), 4);
assert_eq!(raw.len(), 36 + 8);
assert_eq!(bytes_written, raw.len());
}
#[test]
fn f16_writer_rejects_non_finite_or_overflowed_encoding_before_create() {
let tmp = tempfile::tempdir().unwrap();
for (label, value) in [
("nan", f64::NAN),
("positive-infinity", f64::INFINITY),
("negative-infinity", f64::NEG_INFINITY),
("f32-overflow", f64::MAX),
("f16-overflow", 100_000.0_f64),
] {
let path = tmp.path().join(format!("{label}.f16"));
let err = write_f16_file(
&path,
"fixture.safetensors",
"fixture.weight",
&[value],
&[1],
)
.unwrap_err();
let message = err.to_string();
assert!(
matches!(err, InferenceError::InvalidInput(_)),
"{label}: got {err:?}"
);
assert!(message.contains("non-finite value"), "{label}: got {err}");
assert!(message.contains("fixture.safetensors"));
assert!(message.contains("fixture.weight"));
assert!(
!path.exists(),
"{label}: invalid f16 encoding must be rejected before file creation"
);
}
}
#[test]
fn q4_f32_to_f16_canonical_and_subnormal_values() {
assert_eq!(q4_f32_to_f16(0.0), 0x0000);
assert_eq!(q4_f32_to_f16(-0.0), 0x8000);
assert_eq!(q4_f32_to_f16(1.0), 0x3c00);
assert_eq!(q4_f32_to_f16(-1.0), 0xbc00);
assert_eq!(q4_f32_to_f16(f32::INFINITY), 0x7c00);
assert_eq!(q4_f32_to_f16(f32::NEG_INFINITY), 0xfc00);
let h = q4_f32_to_f16(1e-7_f32);
assert_ne!(
h, 0,
"1e-7 (an f16 subnormal range value) must not flush to zero"
);
assert_eq!(q4_f32_to_f16(1e-40_f32), 0);
}
#[test]
fn f32_to_bf16_bits_canonical_values() {
assert_eq!(f32_to_bf16_bits(0.0), 0);
assert_eq!(f32_to_bf16_bits(1.0), 0x3f80);
assert_eq!(f32_to_bf16_bits(-1.0), 0xbf80);
}
#[test]
fn convert_quarot_qwen35_rejects_same_input_output_dir() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 50);
let config_before = fs::read(input.join("config.json")).unwrap();
let err = convert_quarot_qwen35(
&input,
&input, &ConversionOptions::default(),
)
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("same path"), "unexpected error: {msg}");
let config_after = fs::read(input.join("config.json")).unwrap();
assert_eq!(
config_before, config_after,
"rejected conversion must not have mutated the source config.json"
);
}
#[test]
fn convert_quarot_qwen35_rejects_same_input_output_dir_via_trailing_slash() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 51);
let same_with_slash = tmp.path().join("input/.");
let err = convert_quarot_qwen35(&input, &same_with_slash, &ConversionOptions::default())
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("same path"), "unexpected error: {msg}");
}
#[test]
fn convert_quarot_qwen35_rejects_non_empty_output_dir() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 52);
fs::create_dir_all(&output).unwrap();
let stale_path = output.join("stale_artifact.q4");
fs::write(&stale_path, b"old-q4-bytes").unwrap();
let err =
convert_quarot_qwen35(&input, &output, &ConversionOptions::default()).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("not empty"), "unexpected error: {msg}");
assert!(stale_path.exists(), "stale file must not be deleted");
let bytes = fs::read(&stale_path).unwrap();
assert_eq!(&bytes[..], b"old-q4-bytes");
}
#[test]
fn convert_quarot_qwen35_dry_run_ignores_same_output_dir() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 60);
let listing_before = list_dir_recursive(&input);
let report = convert_quarot_qwen35(
&input,
&input, &ConversionOptions {
rotation_seed: 0xDEAD_C0DE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: true,
},
)
.unwrap();
assert!(
report.planned_quantized > 0,
"dry-run must compute planned_quantized > 0"
);
assert!(
report.total_bytes_out > 0,
"dry-run must compute total_bytes_out > 0"
);
let listing_after = list_dir_recursive(&input);
assert_eq!(
listing_before, listing_after,
"dry-run must not mutate the directory it shares with input"
);
}
#[test]
fn convert_quarot_qwen35_dry_run_ignores_non_empty_output_dir() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 61);
fs::create_dir_all(&output).unwrap();
let stale = output.join("stale.q4");
fs::write(&stale, b"old-bytes").unwrap();
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xBEEF_FACE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: true,
},
)
.unwrap();
assert!(
report.planned_quantized > 0,
"dry-run must compute planned_quantized > 0"
);
assert!(report.kept_f16 > 0, "dry-run must compute kept_f16 > 0");
assert!(
report.total_bytes_out > 0,
"dry-run must compute total_bytes_out > 0"
);
assert!(stale.exists(), "stale file must not be deleted in dry-run");
assert_eq!(fs::read(&stale).unwrap(), b"old-bytes");
let listing: Vec<_> = fs::read_dir(&output)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(listing.len(), 1, "dry-run must not add files: {listing:?}");
}
fn list_dir_recursive(root: &Path) -> Vec<(PathBuf, u64)> {
fn walk(root: &Path, dir: &Path, out: &mut Vec<(PathBuf, u64)>) {
for entry in fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
let metadata = entry.metadata().unwrap();
if metadata.is_dir() {
walk(root, &path, out);
} else {
let rel = path.strip_prefix(root).unwrap().to_path_buf();
out.push((rel, metadata.len()));
}
}
}
let mut out = Vec::new();
walk(root, root, &mut out);
out.sort();
out
}
#[test]
fn convert_quarot_qwen35_accepts_empty_pre_existing_output_dir() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 53);
fs::create_dir_all(&output).unwrap();
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xABCD_EF01,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
assert!(report.planned_quantized > 0);
assert!(output.join("config.json").exists());
}
fn tiny_cfg_with_mtp(tied: bool) -> Qwen35Config {
let mut cfg = tiny_cfg(tied);
cfg.mtp_num_hidden_layers = 1;
cfg
}
fn tiny_config_json_with_mtp(cfg: &Qwen35Config) -> String {
let base = tiny_config_json(cfg);
let mut root: serde_json::Value = serde_json::from_str(&base).unwrap();
root.get_mut("text_config")
.unwrap()
.as_object_mut()
.unwrap()
.insert(
"mtp_num_hidden_layers".into(),
serde_json::Value::from(cfg.mtp_num_hidden_layers),
);
serde_json::to_string_pretty(&root).unwrap()
}
fn write_mtp_tensors_into(path: &Path, cfg: &Qwen35Config, mut seed: u64) {
let hidden = cfg.hidden_size;
let intermediate = cfg.intermediate_size;
let head_dim = cfg.head_dim;
let mut next = |n: usize| -> Vec<f64> {
seed = seed.wrapping_add(1);
synth_data(n, seed)
};
let vocab = cfg.vocab_size;
let full_q_dim = cfg.full_q_dim();
let full_kv_dim = cfg.full_kv_dim();
let linear_qkv_dim = cfg.linear_qkv_dim();
let linear_output_dim = cfg.linear_output_dim();
let linear_num_heads = cfg.linear_num_key_heads;
let kernel = cfg.linear_conv_kernel_dim;
let mut entries: Vec<(String, Vec<usize>, Vec<f64>)> = Vec::new();
entries.push((
"model.language_model.embed_tokens.weight".into(),
vec![vocab, hidden],
next(vocab * hidden),
));
entries.push((
"model.language_model.norm.weight".into(),
vec![hidden],
next(hidden),
));
if !cfg.tie_word_embeddings {
entries.push((
"lm_head.weight".into(),
vec![vocab, hidden],
next(vocab * hidden),
));
}
for i in 0..cfg.num_hidden_layers {
let prefix = format!("model.language_model.layers.{i}");
entries.push((
format!("{prefix}.input_layernorm.weight"),
vec![hidden],
next(hidden),
));
entries.push((
format!("{prefix}.post_attention_layernorm.weight"),
vec![hidden],
next(hidden),
));
if cfg.is_full_attention(i) {
entries.push((
format!("{prefix}.self_attn.q_proj.weight"),
vec![2 * full_q_dim, hidden],
next(2 * full_q_dim * hidden),
));
entries.push((
format!("{prefix}.self_attn.k_proj.weight"),
vec![full_kv_dim, hidden],
next(full_kv_dim * hidden),
));
entries.push((
format!("{prefix}.self_attn.v_proj.weight"),
vec![full_kv_dim, hidden],
next(full_kv_dim * hidden),
));
entries.push((
format!("{prefix}.self_attn.o_proj.weight"),
vec![hidden, full_q_dim],
next(hidden * full_q_dim),
));
entries.push((
format!("{prefix}.self_attn.q_norm.weight"),
vec![head_dim],
next(head_dim),
));
entries.push((
format!("{prefix}.self_attn.k_norm.weight"),
vec![head_dim],
next(head_dim),
));
} else {
entries.push((
format!("{prefix}.linear_attn.in_proj_qkv.weight"),
vec![linear_qkv_dim, hidden],
next(linear_qkv_dim * hidden),
));
entries.push((
format!("{prefix}.linear_attn.in_proj_z.weight"),
vec![linear_output_dim, hidden],
next(linear_output_dim * hidden),
));
entries.push((
format!("{prefix}.linear_attn.in_proj_b.weight"),
vec![linear_num_heads, hidden],
next(linear_num_heads * hidden),
));
entries.push((
format!("{prefix}.linear_attn.in_proj_a.weight"),
vec![linear_num_heads, hidden],
next(linear_num_heads * hidden),
));
entries.push((
format!("{prefix}.linear_attn.A_log"),
vec![linear_num_heads],
next(linear_num_heads),
));
entries.push((
format!("{prefix}.linear_attn.dt_bias"),
vec![linear_num_heads],
next(linear_num_heads),
));
entries.push((
format!("{prefix}.linear_attn.conv1d.weight"),
vec![linear_qkv_dim, 1, kernel],
next(linear_qkv_dim * kernel),
));
entries.push((
format!("{prefix}.linear_attn.norm.weight"),
vec![linear_output_dim],
next(linear_output_dim),
));
entries.push((
format!("{prefix}.linear_attn.out_proj.weight"),
vec![hidden, linear_output_dim],
next(hidden * linear_output_dim),
));
}
entries.push((
format!("{prefix}.mlp.gate_proj.weight"),
vec![intermediate, hidden],
next(intermediate * hidden),
));
entries.push((
format!("{prefix}.mlp.up_proj.weight"),
vec![intermediate, hidden],
next(intermediate * hidden),
));
entries.push((
format!("{prefix}.mlp.down_proj.weight"),
vec![hidden, intermediate],
next(hidden * intermediate),
));
}
entries.push((
"mtp.fc.weight".into(),
vec![hidden, 2 * hidden],
next(hidden * 2 * hidden),
));
entries.push((
"mtp.layers.0.self_attn.q_proj.weight".into(),
vec![4 * hidden, hidden],
next(4 * hidden * hidden),
));
entries.push((
"mtp.layers.0.self_attn.k_proj.weight".into(),
vec![hidden, hidden],
next(hidden * hidden),
));
entries.push((
"mtp.layers.0.self_attn.v_proj.weight".into(),
vec![hidden, hidden],
next(hidden * hidden),
));
entries.push((
"mtp.layers.0.self_attn.o_proj.weight".into(),
vec![hidden, 2 * hidden],
next(hidden * 2 * hidden),
));
entries.push((
"mtp.layers.0.mlp.gate_proj.weight".into(),
vec![intermediate, hidden],
next(intermediate * hidden),
));
entries.push((
"mtp.layers.0.mlp.up_proj.weight".into(),
vec![intermediate, hidden],
next(intermediate * hidden),
));
entries.push((
"mtp.layers.0.mlp.down_proj.weight".into(),
vec![hidden, intermediate],
next(hidden * intermediate),
));
entries.push((
"mtp.layers.0.input_layernorm.weight".into(),
vec![hidden],
next(hidden),
));
entries.push((
"mtp.layers.0.post_attention_layernorm.weight".into(),
vec![hidden],
next(hidden),
));
entries.push((
"mtp.layers.0.self_attn.q_norm.weight".into(),
vec![head_dim],
next(head_dim),
));
entries.push((
"mtp.layers.0.self_attn.k_norm.weight".into(),
vec![head_dim],
next(head_dim),
));
entries.push(("mtp.norm.weight".into(), vec![hidden], next(hidden)));
entries.push((
"mtp.pre_fc_norm_embedding.weight".into(),
vec![hidden],
next(hidden),
));
entries.push((
"mtp.pre_fc_norm_hidden.weight".into(),
vec![hidden],
next(hidden),
));
let borrowed: Vec<(&str, Vec<usize>, &[f64])> = entries
.iter()
.map(|(n, s, d)| (n.as_str(), s.clone(), d.as_slice()))
.collect();
write_test_safetensors(path, &borrowed);
}
fn write_input_dir_with_mtp(cfg: &Qwen35Config, dir: &Path, seed: u64) {
fs::create_dir_all(dir).unwrap();
fs::write(dir.join("config.json"), tiny_config_json_with_mtp(cfg)).unwrap();
write_mtp_tensors_into(&dir.join("model.safetensors"), cfg, seed);
}
#[test]
fn convert_quarot_qwen35_emits_mtp_files_for_quarot() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg_with_mtp(true);
write_input_dir_with_mtp(&cfg, &input, 70);
let rotation_seed: u64 = 0xC0DE_BABE;
let _report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
let expected_f16 = [
"mtp_fc_weight.f16",
"mtp_layers_0_self_attn_q_proj_weight.f16",
"mtp_layers_0_self_attn_k_proj_weight.f16",
"mtp_layers_0_self_attn_v_proj_weight.f16",
"mtp_layers_0_self_attn_o_proj_weight.f16",
"mtp_layers_0_mlp_gate_proj_weight.f16",
"mtp_layers_0_mlp_up_proj_weight.f16",
"mtp_layers_0_mlp_down_proj_weight.f16",
"mtp_layers_0_input_layernorm_weight.f16",
"mtp_layers_0_post_attention_layernorm_weight.f16",
"mtp_layers_0_self_attn_q_norm_weight.f16",
"mtp_layers_0_self_attn_k_norm_weight.f16",
"mtp_norm_weight.f16",
"mtp_pre_fc_norm_embedding_weight.f16",
"mtp_pre_fc_norm_hidden_weight.f16",
];
for name in &expected_f16 {
assert!(
output.join(name).exists(),
"MTP f16 file must be emitted: {name}"
);
}
for name in &expected_f16 {
let q4_variant = name.replace(".f16", ".q4");
assert!(
!output.join(&q4_variant).exists(),
"MTP Q4 file must NOT be emitted in Phase 1: {q4_variant}"
);
}
let idx_str = fs::read_to_string(output.join("quantize_index.json")).unwrap();
let idx_val: serde_json::Value = serde_json::from_str(&idx_str).unwrap();
assert_eq!(
idx_val
.get("quarot_seed")
.and_then(serde_json::Value::as_u64),
Some(rotation_seed),
"quantize_index.json must carry quarot_seed (ADR-051 contract)"
);
let out_cfg_str = fs::read_to_string(output.join("config.json")).unwrap();
let out_val: serde_json::Value = serde_json::from_str(&out_cfg_str).unwrap();
assert_eq!(
out_val
.get("text_config")
.and_then(|tc| tc.get("mtp_num_hidden_layers"))
.and_then(serde_json::Value::as_u64),
Some(1),
"output config text_config.mtp_num_hidden_layers must be 1"
);
assert_eq!(
out_val
.get("quarot_rotation_seed")
.and_then(serde_json::Value::as_u64),
Some(rotation_seed),
"output config must carry quarot_rotation_seed at top level"
);
assert_eq!(
out_val
.get("text_config")
.and_then(|tc| tc.get("quarot_rotation_seed"))
.and_then(serde_json::Value::as_u64),
Some(rotation_seed),
"output config text_config must carry quarot_rotation_seed"
);
}
#[test]
fn quarot_mtp_counter_rotation_roundtrip() {
use crate::quant::quarot::hadamard::RandomizedHadamard;
let hidden = 8usize; let seed: u64 = 0xDEAD_C0DE;
let rot = RandomizedHadamard::new(seed, hidden).unwrap();
let original: Vec<f32> = (0..hidden).map(|i| (i as f32 * 0.31 + 0.7).cos()).collect();
let mut data = original.clone();
rot.apply_inverse(&mut data).unwrap();
rot.apply(&mut data).unwrap();
for (i, (got, expected)) in data.iter().zip(original.iter()).enumerate() {
assert!(
(got - expected).abs() < 1e-4,
"roundtrip failed at index {i}: got={got}, expected={expected}"
);
}
}
#[test]
fn inject_quarot_seed_roundtrips_in_config_json() {
let json = r#"{"text_config": {"hidden_size": 8}, "some_key": 1}"#;
let seed: u64 = 0xCAFE_BABE;
let output = inject_quarot_seed(json, seed).unwrap();
let val: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(
val.get("quarot_rotation_seed")
.and_then(serde_json::Value::as_u64),
Some(seed),
"quarot_rotation_seed must be at top level"
);
assert_eq!(
val.get("text_config")
.and_then(|tc| tc.get("quarot_rotation_seed"))
.and_then(serde_json::Value::as_u64),
Some(seed),
"quarot_rotation_seed must be inside text_config"
);
}
#[test]
fn convert_quarot_qwen35_skips_mtp_when_tensors_missing() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg_with_mtp(true);
fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json_with_mtp(&cfg)).unwrap();
write_required_tensors_for(&cfg, &input.join("model.safetensors"), 71, false);
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xDEAD_BABE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
let entries: Vec<_> = fs::read_dir(&output)
.unwrap()
.filter_map(std::result::Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
for name in &entries {
assert!(
!name.starts_with("mtp"),
"unexpected MTP file written when tensors were absent: {name}"
);
}
assert!(report.planned_quantized > 0);
assert!(output.join("config.json").exists());
}
#[test]
fn convert_quarot_qwen35_skips_mtp_when_config_has_zero_layers() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let mut cfg = tiny_cfg(true);
cfg.mtp_num_hidden_layers = 0;
assert_eq!(cfg.mtp_num_hidden_layers, 0);
fs::create_dir_all(&input).unwrap();
fs::write(input.join("config.json"), tiny_config_json(&cfg)).unwrap();
write_required_tensors_for(&cfg, &input.join("model.safetensors"), 72, false);
let report = convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xCAFE_F00D,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
let entries: Vec<_> = fs::read_dir(&output)
.unwrap()
.filter_map(std::result::Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
for name in &entries {
assert!(
!name.starts_with("mtp"),
"unexpected MTP file written for zero-MTP-layer config: {name}"
);
}
assert!(report.planned_quantized > 0);
}
#[test]
fn read_quarot_seed_from_index_absent_file_is_none() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()),
Ok(None),
"missing quantize_index.json must yield Ok(None), not an error"
);
}
#[test]
fn read_quarot_seed_from_index_without_key_is_none() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), r#"{"tensors":[]}"#).unwrap();
assert_eq!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()),
Ok(None),
"index without quarot_seed key must yield Ok(None)"
);
}
#[test]
fn read_quarot_seed_from_index_bare_array_is_none() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"[{"name":"foo","file":"foo.q4","quantized":true,"shape":[2,2],"numel":4}]"#,
)
.unwrap();
assert_eq!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()),
Ok(None),
"bare tensor-array quantize_index.json (plain quantize_q4 shape) must yield Ok(None)"
);
}
#[test]
fn read_quarot_seed_from_index_finds_seed() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":13258600446175248384,"tensors":[]}"#,
)
.unwrap();
assert_eq!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()),
Ok(Some(13_258_600_446_175_248_384_u64)),
"index with quarot_seed key must round-trip the u64 exactly"
);
}
#[test]
fn read_quarot_seed_from_index_rejects_malformed_json() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), "not json").unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b())
.expect_err("malformed quantize_index.json must be rejected, not silently None");
assert!(
err.contains("malformed quantize_index.json"),
"error must name the malformed-index failure; got: {err}"
);
}
#[test]
fn read_quarot_seed_from_index_rejects_incomplete_object_form_entry() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":42,"tensors":[{"name":"foo","file":"foo.q4"}]}"#,
)
.unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()).expect_err(
"object-form manifest with an incomplete tensor entry must be rejected, \
not silently accepted",
);
assert!(
err.contains("malformed quantize_index.json"),
"error must name the malformed-index failure; got: {err}"
);
}
#[test]
fn read_quarot_seed_from_index_bare_array_with_malformed_entries_is_none() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"[{"name":"foo"}, "not even an object", 42]"#,
)
.unwrap();
assert_eq!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()),
Ok(None),
"a bare array with malformed entries still carries no rotation seed \
and must yield Ok(None), not an error"
);
}
#[test]
fn read_quarot_seed_from_index_rejects_wrong_schema_shape() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":42,"tensors":"not-an-array"}"#,
)
.unwrap();
assert!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()).is_err(),
"schema-shape mismatch (tensors not an array) must be rejected"
);
}
#[test]
fn read_quarot_seed_from_index_rejects_truncated_file() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":42,"tensors":[{"name":"foo","file":"foo.q4","quant"#,
)
.unwrap();
assert!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()).is_err(),
"truncated quantize_index.json must be rejected"
);
}
#[test]
fn read_quarot_seed_from_index_rejects_oversized_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("quantize_index.json");
let f = fs::File::create(&path).unwrap();
f.set_len(crate::quant::q4_manifest::MAX_QUANTIZE_INDEX_LEN + 1)
.unwrap();
drop(f);
let err = read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b())
.expect_err("oversized quantize_index.json must be rejected");
assert!(
err.contains("too large"),
"error must name the size-cap failure; got: {err}"
);
}
#[test]
fn quantize_index_online_field_round_trips_through_json() {
let cfg = Qwen35Config::qwen35_0_8b();
let r3 =
crate::quant::quarot::plan::OnlineRotationSpec::r3_full_attention(&cfg, 42, 8).unwrap();
let names: Vec<String> = (0..cfg.num_hidden_layers)
.filter(|&i| cfg.is_full_attention(i))
.map(|i| {
format!(
"{}.self_attn.o_proj.weight",
crate::model::qwen35::qwen_layer_tensor_prefix(i)
)
})
.collect();
let descriptor = OnlineArtifactDescriptor {
version: crate::quant::quarot::io::ArtifactVersion::V1Online,
online_rotations: vec![r3],
asymmetric_tensor_names: names,
};
let index = QuantizeIndex {
quarot_seed: Some(7),
tensors: vec![],
online: Some(descriptor.clone()),
artifact_version: Some(crate::quant::quarot::io::ArtifactVersion::V1Online),
promotion: PromotionRecord::default(),
};
let json = serde_json::to_string(&index).unwrap();
let round_tripped: QuantizeIndex = serde_json::from_str(&json).unwrap();
assert_eq!(round_tripped.quarot_seed, Some(7));
assert_eq!(round_tripped.online, Some(descriptor));
assert_eq!(
round_tripped.artifact_version,
Some(crate::quant::quarot::io::ArtifactVersion::V1Online)
);
}
#[test]
fn quantize_index_v1_online_manifest_is_rejected_at_load_not_silently_accepted() {
let cfg = Qwen35Config::qwen35_0_8b();
let r3 =
crate::quant::quarot::plan::OnlineRotationSpec::r3_full_attention(&cfg, 42, 8).unwrap();
let names: Vec<String> = (0..cfg.num_hidden_layers)
.filter(|&i| cfg.is_full_attention(i))
.map(|i| {
format!(
"{}.self_attn.o_proj.weight",
crate::model::qwen35::qwen_layer_tensor_prefix(i)
)
})
.collect();
let descriptor = OnlineArtifactDescriptor {
version: crate::quant::quarot::io::ArtifactVersion::V1Online,
online_rotations: vec![r3],
asymmetric_tensor_names: names,
};
let index = QuantizeIndex {
quarot_seed: Some(7),
tensors: vec![],
online: Some(descriptor),
artifact_version: Some(crate::quant::quarot::io::ArtifactVersion::V1Online),
promotion: PromotionRecord::default(),
};
let json = serde_json::to_string(&index).unwrap();
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), &json).unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &cfg).expect_err(
"a well-formed V1Online manifest must be rejected at load, not accepted as V0",
);
assert!(
err.contains("does not yet execute V1 online rotation recipes"),
"got: {err}"
);
}
#[test]
fn quantize_index_without_online_field_parses_identically_to_v0() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":13258600446175248384,"tensors":[]}"#,
)
.unwrap();
assert_eq!(
read_quarot_seed_from_index(tmp.path(), &Qwen35Config::qwen35_0_8b()),
Ok(Some(13_258_600_446_175_248_384_u64)),
"a V0 manifest with no online field must parse exactly as before"
);
}
#[test]
fn quantize_index_with_invalid_online_descriptor_is_rejected_at_load() {
let cfg = Qwen35Config::qwen35_0_8b();
let r3 =
crate::quant::quarot::plan::OnlineRotationSpec::r3_full_attention(&cfg, 42, 8).unwrap();
let invalid_descriptor = OnlineArtifactDescriptor {
version: crate::quant::quarot::io::ArtifactVersion::V1Online,
online_rotations: vec![r3],
asymmetric_tensor_names: vec![],
};
let index = QuantizeIndex {
quarot_seed: Some(7),
tensors: vec![],
online: Some(invalid_descriptor),
artifact_version: Some(crate::quant::quarot::io::ArtifactVersion::V1Online),
promotion: PromotionRecord::default(),
};
let json = serde_json::to_string(&index).unwrap();
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), &json).unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &cfg)
.expect_err("a manifest carrying an invalid online descriptor must be rejected");
assert!(
err.contains("does not yet execute V1 online rotation recipes"),
"got: {err}"
);
}
#[test]
fn quantize_index_v1_online_large_layer_scope_is_rejected_without_running_validate() {
let cfg = Qwen35Config::qwen35_0_8b();
let adversarial_layers: Vec<usize> = (0..cfg.num_hidden_layers * 4).collect();
let r3 = crate::quant::quarot::plan::OnlineRotationSpec {
id: crate::quant::quarot::plan::RotationId::AttentionOutputR3,
side: crate::quant::quarot::plan::AbsorptionSide::InputSide,
seed: 42,
block_size: 8,
layer_scope: Some(adversarial_layers),
};
let descriptor = OnlineArtifactDescriptor {
version: crate::quant::quarot::io::ArtifactVersion::V1Online,
online_rotations: vec![r3],
asymmetric_tensor_names: vec![],
};
let index = QuantizeIndex {
quarot_seed: Some(7),
tensors: vec![],
online: Some(descriptor),
artifact_version: Some(crate::quant::quarot::io::ArtifactVersion::V1Online),
promotion: PromotionRecord::default(),
};
let json = serde_json::to_string(&index).unwrap();
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), &json).unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &cfg)
.expect_err("an adversarially-shaped V1Online manifest must still be rejected");
assert!(
err.contains("does not yet execute V1 online rotation recipes"),
"expected the unconditional version-reject message (proving \
`validate`'s per-spec scan did not run and produce its own \
error instead), got: {err}"
);
}
#[test]
fn quantize_index_v1_version_without_online_key_is_rejected_fail_closed() {
let cfg = Qwen35Config::qwen35_0_8b();
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":7,"tensors":[],"artifact_version":"v1-online-r3r4"}"#,
)
.unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &cfg).expect_err(
"a manifest declaring artifact_version v1-online-r3r4 with no online \
key must be rejected, not silently loaded as V0",
);
assert!(
err.contains("rotation descriptor is missing or null"),
"got: {err}"
);
}
#[test]
fn quantize_index_v1_version_with_null_online_is_rejected_fail_closed() {
let cfg = Qwen35Config::qwen35_0_8b();
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":7,"tensors":[],"online":null,"artifact_version":"v1-online-r3r4"}"#,
)
.unwrap();
let err = read_quarot_seed_from_index(tmp.path(), &cfg).expect_err(
"a manifest declaring artifact_version v1-online-r3r4 with online \
explicitly null must be rejected, not silently loaded as V0",
);
assert!(
err.contains("rotation descriptor is missing or null"),
"got: {err}"
);
}
#[test]
fn convert_quarot_qwen35_writes_unpromoted_promotion_marker() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 1);
convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xC0FFEE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: false,
},
)
.unwrap();
let record = read_promotion_record(&output).unwrap();
assert_eq!(
record.state,
PromotionState::Unpromoted,
"a fresh conversion must not claim promoted/rejected before the PPL \
acceptance gate has been recorded against it"
);
assert!(
record.ppl_gate.is_none(),
"no PPL gate has run yet; ppl_gate must be absent"
);
assert!(
record.reason.contains("PPL acceptance gate"),
"reason must explain WHY the artifact is unpromoted, got: {}",
record.reason
);
let idx_str = fs::read_to_string(output.join("quantize_index.json")).unwrap();
let idx: Value = serde_json::from_str(&idx_str).unwrap();
assert_eq!(
idx.get("promotion").and_then(|p| p.get("state")),
Some(&Value::String("unpromoted".to_string())),
"quantize_index.json must carry a top-level, human-readable promotion.state"
);
}
#[test]
fn convert_quarot_qwen35_dry_run_writes_no_promotion_marker() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("input");
let output = tmp.path().join("output");
let cfg = tiny_cfg(true);
write_input_dir(&cfg, &input, 1);
convert_quarot_qwen35(
&input,
&output,
&ConversionOptions {
rotation_seed: 0xC0FFEE,
tolerance: 1e-5,
num_probe_tokens: 2,
dry_run: true,
},
)
.unwrap();
assert!(
!output.join("quantize_index.json").exists(),
"dry-run must not write any files, including the promotion marker"
);
}
fn object_form_manifest_json(promoted_state: Option<&str>) -> String {
let mut obj = serde_json::json!({
"quarot_seed": 42,
"tensors": [],
});
if let Some(state) = promoted_state {
obj["promotion"] = serde_json::json!({"state": state, "reason": "test fixture"});
}
serde_json::to_string(&obj).unwrap()
}
#[test]
fn record_ppl_gate_result_promotes_on_pass() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
object_form_manifest_json(None),
)
.unwrap();
let record = record_ppl_gate_result(tmp.path(), 25.0, 24.5, 0.5).unwrap();
assert_eq!(record.state, PromotionState::Promoted);
let gate = record.ppl_gate.clone().expect("ppl_gate must be recorded");
assert_eq!(gate.unrotated_ppl, 25.0);
assert_eq!(gate.quarot_ppl, 24.5);
assert!((gate.delta - (-0.5)).abs() < 1e-12);
assert_eq!(gate.delta_threshold, 0.5);
let reread = read_promotion_record(tmp.path()).unwrap();
assert_eq!(reread, record);
}
#[test]
fn record_ppl_gate_result_rejects_on_fail() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
object_form_manifest_json(None),
)
.unwrap();
let record = record_ppl_gate_result(tmp.path(), 25.0, 25.5, 0.5).unwrap();
assert_eq!(record.state, PromotionState::Rejected);
let reread = read_promotion_record(tmp.path()).unwrap();
assert_eq!(reread.state, PromotionState::Rejected);
}
#[test]
fn record_ppl_gate_result_overwrites_prior_state() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
object_form_manifest_json(Some("rejected")),
)
.unwrap();
let record = record_ppl_gate_result(tmp.path(), 25.0, 24.0, 0.5).unwrap();
assert_eq!(record.state, PromotionState::Promoted);
let reread = read_promotion_record(tmp.path()).unwrap();
assert_eq!(reread.state, PromotionState::Promoted);
}
#[test]
fn record_ppl_gate_result_rejects_bare_array_manifest() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), r#"[]"#).unwrap();
let err = record_ppl_gate_result(tmp.path(), 25.0, 24.0, 0.5)
.expect_err("a bare-array manifest must be rejected, not silently accepted");
assert!(err.to_string().contains("bare-array"), "got: {err}");
}
#[test]
fn record_ppl_gate_result_errs_on_missing_manifest() {
let tmp = tempfile::tempdir().unwrap();
let err = record_ppl_gate_result(tmp.path(), 25.0, 24.0, 0.5)
.expect_err("recording against a directory with no manifest must fail closed");
assert!(err.to_string().contains("failed to read"), "got: {err}");
}
#[test]
fn read_promotion_record_defaults_to_unpromoted_for_legacy_manifest() {
let tmp = tempfile::tempdir().unwrap();
fs::write(
tmp.path().join("quantize_index.json"),
r#"{"quarot_seed":7,"tensors":[]}"#,
)
.unwrap();
let record = read_promotion_record(tmp.path()).unwrap();
assert_eq!(record.state, PromotionState::Unpromoted);
assert!(record.reason.contains("predates"));
}
#[test]
fn read_promotion_record_rejects_bare_array_manifest() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("quantize_index.json"), r#"[]"#).unwrap();
let err = read_promotion_record(tmp.path())
.expect_err("a bare-array manifest has no promotion field and must be rejected");
assert!(err.to_string().contains("bare-array"), "got: {err}");
}
}