use crate::format::apr2::Lfm2Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WasmQuantization {
Fp16,
Int8,
Int4Awq,
Int4Gptq,
}
impl WasmQuantization {
#[must_use]
pub const fn bytes_per_param(&self) -> f32 {
match self {
Self::Fp16 => 2.0,
Self::Int8 => 1.0,
Self::Int4Awq | Self::Int4Gptq => 0.5,
}
}
#[must_use]
pub fn is_wasm_viable(&self, num_params: u64) -> bool {
let model_bytes = (num_params as f64) * (self.bytes_per_param() as f64);
model_bytes < 2_000_000_000.0 }
}
impl std::fmt::Display for WasmQuantization {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fp16 => write!(f, "fp16"),
Self::Int8 => write!(f, "int8"),
Self::Int4Awq => write!(f, "int4-awq"),
Self::Int4Gptq => write!(f, "int4-gptq"),
}
}
}
#[derive(Debug, Clone)]
pub struct Lfm2WasmConfig {
pub quantization: WasmQuantization,
pub max_context: usize,
pub sliding_window: Option<usize>,
pub use_webgpu: bool,
pub streaming: bool,
}
impl Default for Lfm2WasmConfig {
fn default() -> Self {
Self {
quantization: WasmQuantization::Int4Awq,
max_context: 4096,
sliding_window: Some(2048),
use_webgpu: true,
streaming: true,
}
}
}
impl Lfm2WasmConfig {
#[must_use]
pub fn lfm2_2_6b() -> Self {
Self::default()
}
#[must_use]
pub fn full_attention() -> Self {
Self {
sliding_window: None,
..Self::default()
}
}
#[must_use]
pub fn low_memory() -> Self {
Self {
quantization: WasmQuantization::Int4Awq,
max_context: 2048,
sliding_window: Some(1024),
use_webgpu: true,
streaming: true,
}
}
}
#[derive(Debug, Clone)]
pub struct WasmMemoryEstimate {
pub model_bytes: u64,
pub kv_cache_bytes: u64,
pub overhead_bytes: u64,
pub total_bytes: u64,
pub is_viable: bool,
pub warnings: Vec<String>,
}
impl WasmMemoryEstimate {
#[must_use]
pub fn calculate(config: &Lfm2Config, wasm_config: &Lfm2WasmConfig) -> Self {
let num_params: u64 = 2_600_000_000;
let model_bytes =
(num_params as f64 * wasm_config.quantization.bytes_per_param() as f64) as u64;
let num_layers = config.num_layers as u64;
let num_kv_heads = config.num_kv_heads as u64;
let head_dim = (config.hidden_size / config.num_q_heads) as u64;
let bytes_per_token = 2 * num_layers * num_kv_heads * head_dim * 2;
let effective_context = wasm_config
.sliding_window
.unwrap_or(wasm_config.max_context) as u64;
let kv_cache_bytes = bytes_per_token * effective_context;
let overhead_bytes: u64 = 200_000_000;
let total_bytes = model_bytes + kv_cache_bytes + overhead_bytes;
let browser_limit: u64 = 2_000_000_000; let is_viable = total_bytes < browser_limit;
let mut warnings = Vec::new();
if !is_viable {
warnings.push(format!(
"Total memory ({:.2} GB) exceeds browser limit (~2 GB)",
total_bytes as f64 / 1_000_000_000.0
));
}
if matches!(wasm_config.quantization, WasmQuantization::Fp16) {
warnings.push("fp16 quantization exceeds WASM memory limits".to_string());
}
if wasm_config.max_context > 8192 {
warnings.push(format!(
"Large context ({}) may cause OOM in browser",
wasm_config.max_context
));
}
if wasm_config.sliding_window.is_none() && wasm_config.max_context > 4096 {
warnings.push("Full attention with >4K context may exceed memory".to_string());
}
Self {
model_bytes,
kv_cache_bytes,
overhead_bytes,
total_bytes,
is_viable,
warnings,
}
}
#[must_use]
pub fn summary(&self) -> String {
use std::fmt::Write;
let mut s = String::new();
let _ = writeln!(
s,
"Model: {:>7.2} GB",
self.model_bytes as f64 / 1_000_000_000.0
);
let _ = writeln!(
s,
"KV Cache: {:>7.2} GB",
self.kv_cache_bytes as f64 / 1_000_000_000.0
);
let _ = writeln!(
s,
"Overhead: {:>7.2} GB",
self.overhead_bytes as f64 / 1_000_000_000.0
);
s.push_str("─────────────────\n");
let _ = writeln!(
s,
"Total: {:>7.2} GB {}",
self.total_bytes as f64 / 1_000_000_000.0,
if self.is_viable { "✅" } else { "❌" }
);
if !self.warnings.is_empty() {
s.push_str("\nWarnings:\n");
for w in &self.warnings {
let _ = writeln!(s, " ⚠️ {w}");
}
}
s
}
}
impl std::fmt::Display for WasmMemoryEstimate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.summary())
}
}