use crate::gguf::GgmlType;
#[allow(non_camel_case_types)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QuantTier {
F32,
Q8_0,
Q6_K,
Q5_K,
Q4_K,
Q3_K,
Q2_K,
}
impl QuantTier {
#[must_use]
pub const fn to_ggml(self) -> GgmlType {
match self {
Self::F32 => GgmlType::F32,
Self::Q8_0 => GgmlType::Q8_0,
Self::Q6_K => GgmlType::Q6_K,
Self::Q5_K => GgmlType::Q5_K,
Self::Q4_K => GgmlType::Q4_K,
Self::Q3_K => GgmlType::Q3_K,
Self::Q2_K => GgmlType::Q2_K,
}
}
#[must_use]
pub const fn bpw(self) -> f32 {
match self {
Self::F32 => 32.0,
Self::Q8_0 => 8.5,
Self::Q6_K => 6.5625,
Self::Q5_K => 5.5,
Self::Q4_K => 4.5,
Self::Q3_K => 3.4375,
Self::Q2_K => 2.625,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::F32 => "F32",
Self::Q8_0 => "Q8_0",
Self::Q6_K => "Q6_K",
Self::Q5_K => "Q5_K",
Self::Q4_K => "Q4_K",
Self::Q3_K => "Q3_K",
Self::Q2_K => "Q2_K",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SizeBudget {
bytes: u64,
}
impl SizeBudget {
#[must_use]
pub const fn from_bytes(bytes: u64) -> Option<Self> {
if bytes == 0 {
None
} else {
Some(Self { bytes })
}
}
#[must_use]
pub fn total_gb(self) -> f32 {
self.bytes as f32 / 1_000_000_000.0
}
#[must_use]
pub const fn total_bytes(self) -> u64 {
self.bytes
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TierBand {
High,
Mid,
MidLow,
Low,
}
#[derive(Debug, Clone, Copy)]
pub struct TierMenu {
pub high: QuantTier,
pub mid: QuantTier,
pub mid_low: QuantTier,
pub low: QuantTier,
}
impl TierMenu {
#[must_use]
pub fn for_budget(budget: SizeBudget) -> Self {
let gb = budget.total_gb();
if gb >= 8.0 {
Self {
high: QuantTier::Q6_K,
mid: QuantTier::Q5_K,
mid_low: QuantTier::Q4_K,
low: QuantTier::Q3_K,
}
} else if gb >= 4.0 {
Self {
high: QuantTier::Q5_K,
mid: QuantTier::Q4_K,
mid_low: QuantTier::Q3_K,
low: QuantTier::Q3_K,
}
} else {
Self {
high: QuantTier::Q4_K,
mid: QuantTier::Q3_K,
mid_low: QuantTier::Q2_K,
low: QuantTier::Q2_K,
}
}
}
#[must_use]
pub const fn resolve(&self, band: TierBand) -> QuantTier {
match band {
TierBand::High => self.high,
TierBand::Mid => self.mid,
TierBand::MidLow => self.mid_low,
TierBand::Low => self.low,
}
}
#[must_use]
pub const fn bump(tier: QuantTier) -> QuantTier {
match tier {
QuantTier::F32 | QuantTier::Q8_0 => QuantTier::Q8_0,
QuantTier::Q6_K => QuantTier::Q8_0,
QuantTier::Q5_K => QuantTier::Q6_K,
QuantTier::Q4_K => QuantTier::Q5_K,
QuantTier::Q3_K => QuantTier::Q4_K,
QuantTier::Q2_K => QuantTier::Q3_K,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn budget_from_bytes_rejects_zero() {
assert!(SizeBudget::from_bytes(0).is_none());
assert!(SizeBudget::from_bytes(1).is_some());
}
#[test]
fn budget_gb_conversion_matches_unsloth_labels() {
let b = SizeBudget::from_bytes(9_830_000_000).expect("nonzero");
assert!((b.total_gb() - 9.83).abs() < 0.01);
}
#[test]
fn menu_high_budget_reserves_q6k_headroom() {
let menu = TierMenu::for_budget(SizeBudget::from_bytes(10_000_000_000).expect("nonzero"));
assert_eq!(menu.high, QuantTier::Q6_K);
assert_eq!(menu.low, QuantTier::Q3_K);
}
#[test]
fn menu_mid_budget_uses_q5k_high() {
let menu = TierMenu::for_budget(SizeBudget::from_bytes(5_000_000_000).expect("nonzero"));
assert_eq!(menu.high, QuantTier::Q5_K);
assert_eq!(menu.mid, QuantTier::Q4_K);
}
#[test]
fn menu_low_budget_collapses_to_q2k_floor() {
let menu = TierMenu::for_budget(SizeBudget::from_bytes(3_500_000_000).expect("nonzero"));
assert_eq!(menu.high, QuantTier::Q4_K);
assert_eq!(menu.low, QuantTier::Q2_K);
}
#[test]
fn bump_saturates_at_q8_0() {
assert_eq!(TierMenu::bump(QuantTier::Q8_0), QuantTier::Q8_0);
assert_eq!(TierMenu::bump(QuantTier::Q6_K), QuantTier::Q8_0);
assert_eq!(TierMenu::bump(QuantTier::Q5_K), QuantTier::Q6_K);
assert_eq!(TierMenu::bump(QuantTier::Q2_K), QuantTier::Q3_K);
}
#[test]
fn tier_bpw_is_monotonic() {
let tiers = [
QuantTier::Q2_K,
QuantTier::Q3_K,
QuantTier::Q4_K,
QuantTier::Q5_K,
QuantTier::Q6_K,
QuantTier::Q8_0,
QuantTier::F32,
];
for pair in tiers.windows(2) {
assert!(
pair[0].bpw() < pair[1].bpw(),
"bpw not monotonic between {pair:?}",
);
}
}
}