use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{debug, info};
use super::fingerprint::ModelFingerprint;
use crate::core::hardware::HardwareProfile;
#[derive(Error, Debug)]
pub enum HeuristicsError {
#[error("Heuristic resolution failed: {reason}")]
ResolutionFailed { reason: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeuristicResult {
pub quant_method: String,
pub bits: u8,
pub group_size: usize,
pub confidence: f64,
pub reasoning: String,
}
const MEMORY_HEADROOM_FACTOR: f64 = 1.3;
const GENEROUS_HEADROOM_FACTOR: f64 = 1.8;
const DEFAULT_GROUP_SIZE: usize = 64;
#[allow(dead_code)]
pub fn select_quant(
hardware: &HardwareProfile,
fingerprint: &ModelFingerprint,
) -> Result<HeuristicResult, HeuristicsError> {
select_quant_with_format(hardware, fingerprint, None)
}
pub fn select_quant_with_format(
hardware: &HardwareProfile,
fingerprint: &ModelFingerprint,
format: Option<super::OutputFormatHint>,
) -> Result<HeuristicResult, HeuristicsError> {
let available_bytes = hardware.available_memory_bytes;
let total_bytes = hardware.total_memory_bytes;
let memory_budget = available_bytes.max((total_bytes as f64 * 0.7) as u64);
let f16_size = fingerprint.estimated_f16_size_bytes();
let q8_size = fingerprint.estimated_size_bytes(8);
let q4_size = fingerprint.estimated_size_bytes(4);
let q2_size = fingerprint.estimated_size_bytes(2);
let is_moe = fingerprint.is_moe();
debug!(
memory_budget_gb = memory_budget as f64 / 1e9,
f16_size_gb = f16_size as f64 / 1e9,
q8_size_gb = q8_size as f64 / 1e9,
q4_size_gb = q4_size as f64 / 1e9,
q2_size_gb = q2_size as f64 / 1e9,
is_moe = is_moe,
"Heuristic memory analysis"
);
if is_moe && q4_size as f64 * MEMORY_HEADROOM_FACTOR <= memory_budget as f64 {
let method = match format {
Some(super::OutputFormatHint::Gguf) => "apex",
_ => "mixed-4-6",
};
let confidence = 0.75;
let result = HeuristicResult {
quant_method: method.to_string(),
bits: 4,
group_size: DEFAULT_GROUP_SIZE,
confidence,
reasoning: format!(
"MoE model ({:.1} GB at q4) fits in memory ({:.1} GB). \
{} preserves router precision while compressing expert FFNs. \
Expert redundancy makes this architecture resilient to quantization.",
q4_size as f64 / 1e9,
memory_budget as f64 / 1e9,
method,
),
};
info!(
method = method,
confidence = confidence,
"Heuristic: {} — MoE architecture with format-aware selection",
method
);
return Ok(result);
}
if f16_size as f64 * GENEROUS_HEADROOM_FACTOR <= memory_budget as f64 {
let confidence = 0.9;
let result = HeuristicResult {
quant_method: "f16".to_string(),
bits: 16,
group_size: 0,
confidence,
reasoning: format!(
"Model ({:.1} GB at f16) fits comfortably in available memory ({:.1} GB) with generous headroom. \
f16 preserves full precision.",
f16_size as f64 / 1e9,
memory_budget as f64 / 1e9,
),
};
info!(
method = "f16",
confidence = confidence,
"Heuristic: f16 — model fits with generous headroom"
);
return Ok(result);
}
if f16_size as f64 * MEMORY_HEADROOM_FACTOR <= memory_budget as f64 {
let confidence = 0.75;
let result = HeuristicResult {
quant_method: "q8".to_string(),
bits: 8,
group_size: DEFAULT_GROUP_SIZE,
confidence,
reasoning: format!(
"Model ({:.1} GB at f16) fits in memory ({:.1} GB) but without generous headroom. \
q8 reduces size by 2x with minimal quality loss.",
f16_size as f64 / 1e9,
memory_budget as f64 / 1e9,
),
};
info!(
method = "q8",
confidence = confidence,
"Heuristic: q8 — model fits at f16 but tight"
);
return Ok(result);
}
if q8_size as f64 * MEMORY_HEADROOM_FACTOR <= memory_budget as f64 {
let confidence = 0.7;
let method = match format {
Some(super::OutputFormatHint::Gguf) if fingerprint.total_params >= 3_000_000_000 => {
"apex"
}
_ => "mixed-4-6",
};
let result = HeuristicResult {
quant_method: method.to_string(),
bits: 4,
group_size: DEFAULT_GROUP_SIZE,
confidence,
reasoning: format!(
"Model ({:.1} GB at q8) fits with headroom in available memory ({:.1} GB). \
{} gives good quality with ~4x compression from f16.",
q8_size as f64 / 1e9,
memory_budget as f64 / 1e9,
method,
),
};
info!(
method = method,
confidence = confidence,
"Heuristic: {} — q8 fits but want better compression",
method
);
return Ok(result);
}
if q4_size as f64 * MEMORY_HEADROOM_FACTOR <= memory_budget as f64 {
let confidence = 0.65;
let result = HeuristicResult {
quant_method: "q4".to_string(),
bits: 4,
group_size: DEFAULT_GROUP_SIZE,
confidence,
reasoning: format!(
"Model ({:.1} GB at q4) fits in available memory ({:.1} GB). \
q4 provides ~4x compression from f16 with acceptable quality loss.",
q4_size as f64 / 1e9,
memory_budget as f64 / 1e9,
),
};
info!(
method = "q4",
confidence = confidence,
"Heuristic: q4 — tight memory, standard quantization"
);
return Ok(result);
}
if q2_size as f64 * MEMORY_HEADROOM_FACTOR <= memory_budget as f64 {
let confidence = 0.5;
let result = HeuristicResult {
quant_method: "q2".to_string(),
bits: 2,
group_size: DEFAULT_GROUP_SIZE,
confidence,
reasoning: format!(
"Model requires aggressive quantization to fit in available memory ({:.1} GB). \
q2 provides ~8x compression from f16 but significant quality loss is expected.",
memory_budget as f64 / 1e9,
),
};
info!(
method = "q2",
confidence = confidence,
"Heuristic: q2 — very tight memory"
);
return Ok(result);
}
Err(HeuristicsError::ResolutionFailed {
reason: format!(
"Model is too large for available memory even at q2 quantization. \
Estimated q2 size: {:.1} GB, available memory budget: {:.1} GB. \
Consider a smaller model or a machine with more memory.",
q2_size as f64 / 1e9,
memory_budget as f64 / 1e9,
),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_hardware(total_gb: u64, available_gb: u64) -> HardwareProfile {
HardwareProfile {
chip_model: "Apple M5 Max".to_string(),
total_memory_bytes: total_gb * 1024 * 1024 * 1024,
available_memory_bytes: available_gb * 1024 * 1024 * 1024,
performance_cores: 14,
efficiency_cores: 4,
total_cores: 18,
memory_bandwidth_gbs: 540.0,
}
}
fn make_fingerprint(param_billions: f64) -> ModelFingerprint {
ModelFingerprint {
architecture: "TestModel".to_string(),
total_params: (param_billions * 1e9) as u64,
layer_count: 32,
expert_count: 0,
attention_types: vec!["attention".to_string()],
hidden_size: 4096,
dtype: "bfloat16".to_string(),
intermediate_size: Some(14336),
num_attention_heads: 32,
num_kv_heads: Some(8),
vocab_size: 128256,
}
}
#[test]
fn test_small_model_on_large_machine_gets_f16() {
let hw = make_hardware(128, 100);
let fp = make_fingerprint(3.0);
let result = select_quant(&hw, &fp).unwrap();
assert_eq!(result.quant_method, "f16");
assert_eq!(result.bits, 16);
assert!(result.confidence >= 0.8);
}
#[test]
fn test_medium_model_on_large_machine_gets_f16() {
let hw = make_hardware(128, 100);
let fp = make_fingerprint(8.0);
let result = select_quant(&hw, &fp).unwrap();
assert_eq!(result.quant_method, "f16");
}
#[test]
fn test_model_fits_tight_gets_q8() {
let hw = make_hardware(128, 70);
let fp = make_fingerprint(27.0);
let result = select_quant(&hw, &fp).unwrap();
assert_eq!(result.quant_method, "q8");
}
#[test]
fn test_large_model_moderate_memory_gets_mixed() {
let hw = make_hardware(64, 30);
let fp = make_fingerprint(27.0);
let result = select_quant(&hw, &fp).unwrap();
assert_eq!(result.quant_method, "mixed-4-6");
}
#[test]
fn test_large_model_small_memory_gets_q4() {
let hw = make_hardware(36, 20);
let fp = make_fingerprint(27.0);
let result = select_quant(&hw, &fp).unwrap();
assert_eq!(result.quant_method, "q4");
}
#[test]
fn test_huge_model_tiny_memory_gets_q2() {
let hw = make_hardware(36, 20);
let fp = make_fingerprint(70.0);
let result = select_quant(&hw, &fp).unwrap();
assert_eq!(result.quant_method, "q2");
}
#[test]
fn test_model_too_large_errors() {
let hw = make_hardware(36, 20);
let fp = make_fingerprint(405.0);
let result = select_quant(&hw, &fp);
assert!(result.is_err());
}
#[test]
fn test_confidence_decreases_with_more_quantization() {
let hw = make_hardware(128, 100);
let fp_small = make_fingerprint(3.0);
let r_small = select_quant(&hw, &fp_small).unwrap();
let fp_large = make_fingerprint(70.0);
let r_large = select_quant(&hw, &fp_large).unwrap();
assert!(r_small.confidence >= r_large.confidence);
}
#[test]
fn test_reasoning_is_populated() {
let hw = make_hardware(128, 100);
let fp = make_fingerprint(8.0);
let result = select_quant(&hw, &fp).unwrap();
assert!(!result.reasoning.is_empty());
}
}