Eliminate the entire mixed_precision runtime indirection layer: - Delete crates/ml-core/src/mixed_precision.rs (training_dtype, ensure_training_dtype, align_dim_for_tensor_cores) - Inline ~100 call sites across 130 files to constants: training_dtype(&device) → candle_core::DType::BF16 ensure_training_dtype(x) → x.to_dtype(candle_core::DType::BF16) align_dim_for_tensor_cores(x, &device) → (x + 7) & !7 - Remove re-exports from ml-dqn, ml-supervised, ml lib.rs - Clean config/toml/json/shell references No CPU/Metal training path exists — BF16 is the only dtype. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
291 lines
9.6 KiB
Rust
291 lines
9.6 KiB
Rust
//! Quantized Variable Selection Network for TFT
|
|
//!
|
|
//! INT8 quantized implementation of VSN for 4x memory reduction and speedup.
|
|
//! Maintains accuracy within 5% of F32 baseline.
|
|
|
|
use std::collections::HashMap;
|
|
use std::mem::size_of;
|
|
use std::sync::Arc;
|
|
|
|
use candle_core::{DType, Device, Tensor};
|
|
use candle_nn::{VarBuilder, VarMap};
|
|
use tracing::{debug, info};
|
|
|
|
use super::variable_selection::VariableSelectionNetwork;
|
|
#[cfg(test)]
|
|
use ml_core::memory_optimization::quantization::{
|
|
QuantizationConfig, QuantizationType, QuantizedTensor, Quantizer,
|
|
};
|
|
#[cfg(not(test))]
|
|
use ml_core::memory_optimization::quantization::{QuantizationConfig, QuantizedTensor, Quantizer};
|
|
use ml_core::MLError;
|
|
|
|
/// Quantized Variable Selection Network
|
|
///
|
|
/// Converts F32 VSN weights to INT8 (U8 dtype) for memory reduction.
|
|
/// Target: 150MB → 38MB (4x reduction)
|
|
#[derive(Debug, Clone)]
|
|
pub struct QuantizedVariableSelectionNetwork {
|
|
/// Quantized weights from `VarMap`
|
|
quantized_weights: HashMap<String, QuantizedTensor>,
|
|
|
|
/// Original VSN metadata
|
|
hidden_size: usize,
|
|
|
|
/// Device
|
|
device: Device,
|
|
}
|
|
|
|
impl QuantizedVariableSelectionNetwork {
|
|
/// Create quantized VSN from F32 model
|
|
///
|
|
/// Extracts weights from `VarMap` and quantizes them to INT8.
|
|
#[allow(clippy::unwrap_in_result)]
|
|
pub fn from_f32_model(
|
|
vsn: &VariableSelectionNetwork,
|
|
config: QuantizationConfig,
|
|
device: Device,
|
|
) -> Result<Self, MLError> {
|
|
info!(
|
|
"Quantizing VSN (input_size={}, hidden_size={}) to {:?}",
|
|
vsn.input_size, vsn.hidden_size, config.quant_type
|
|
);
|
|
|
|
// Create a temporary VarMap to extract weights
|
|
// We'll create a new VSN to get access to the varmap
|
|
let varmap = Arc::new(VarMap::new());
|
|
let vs = VarBuilder::from_varmap(&varmap, candle_core::DType::BF16, &device);
|
|
|
|
// Create a new VSN with same config to get weight structure
|
|
let _temp_vsn =
|
|
VariableSelectionNetwork::new(vsn.input_size, vsn.hidden_size, vs.pp("temp"))?;
|
|
|
|
// Now quantize all weights from the varmap
|
|
let mut quantizer = Quantizer::new(config, device.clone());
|
|
let mut quantized_weights = HashMap::new();
|
|
|
|
// Get all variables from varmap
|
|
let var_data = varmap.data().lock().map_err(|e| {
|
|
MLError::ModelError(format!("Failed to lock varmap data: {}", e))
|
|
})?;
|
|
let vars = var_data.clone();
|
|
drop(var_data);
|
|
|
|
for (name, tensor) in vars.iter() {
|
|
let quantized = Self::quantize_tensor_to_u8(&mut quantizer, tensor, name)?;
|
|
let dtype = quantized.data.dtype();
|
|
quantized_weights.insert(name.clone(), quantized);
|
|
debug!("Quantized weight: {} -> {:?}", name, dtype);
|
|
}
|
|
|
|
info!("Quantized {} weights from VSN", quantized_weights.len());
|
|
|
|
Ok(Self {
|
|
quantized_weights,
|
|
hidden_size: vsn.hidden_size,
|
|
device,
|
|
})
|
|
}
|
|
|
|
/// Quantize tensor to U8 dtype
|
|
fn quantize_tensor_to_u8(
|
|
quantizer: &mut Quantizer,
|
|
tensor: &Tensor,
|
|
name: &str,
|
|
) -> Result<QuantizedTensor, MLError> {
|
|
// Use quantizer to calculate scale/zero_point
|
|
let quant_result = quantizer.quantize_tensor(tensor, name)?;
|
|
|
|
// Convert to actual U8 dtype
|
|
let u8_data =
|
|
Self::convert_to_u8_dtype(tensor, quant_result.scale, quant_result.zero_point)?;
|
|
|
|
Ok(QuantizedTensor {
|
|
data: u8_data,
|
|
quant_type: quant_result.quant_type,
|
|
scale: quant_result.scale,
|
|
zero_point: quant_result.zero_point,
|
|
})
|
|
}
|
|
|
|
/// Convert F32 tensor to U8 dtype
|
|
fn convert_to_u8_dtype(tensor: &Tensor, scale: f32, zero_point: i8) -> Result<Tensor, MLError> {
|
|
// Quantize: q = clamp(round(x / scale) + zero_point, 0, 255)
|
|
let zero_point_f32 = zero_point as f32;
|
|
let device = tensor.device();
|
|
|
|
// Convert scalars to tensors for operations
|
|
let scale_tensor = Tensor::new(&[scale], device)?;
|
|
let zero_point_tensor = Tensor::new(&[zero_point_f32], device)?;
|
|
|
|
// Divide by scale
|
|
let scaled = tensor.broadcast_div(&scale_tensor)?;
|
|
|
|
// Add zero point
|
|
let shifted = scaled.broadcast_add(&zero_point_tensor)?;
|
|
|
|
// Round to nearest integer
|
|
let rounded = shifted
|
|
.round()
|
|
.map_err(|e| MLError::ModelError(format!("Failed to round tensor: {}", e)))?;
|
|
|
|
// Clamp to [0, 255]
|
|
let clamped = rounded
|
|
.clamp(0.0, 255.0)
|
|
.map_err(|e| MLError::ModelError(format!("Failed to clamp tensor: {}", e)))?;
|
|
|
|
// Convert to U8
|
|
let u8_tensor = clamped
|
|
.to_dtype(DType::U8)
|
|
.map_err(|e| MLError::ModelError(format!("Failed to convert to U8: {}", e)))?;
|
|
|
|
debug!("Converted tensor to U8: shape={:?}", u8_tensor.dims());
|
|
Ok(u8_tensor)
|
|
}
|
|
|
|
/// Forward pass with INT8 weights (placeholder)
|
|
///
|
|
/// Full implementation would require reconstructing VSN computation graph.
|
|
pub fn forward(
|
|
&self,
|
|
input: &Tensor,
|
|
_context: Option<&Tensor>,
|
|
_quantizer: &Quantizer,
|
|
) -> Result<Tensor, MLError> {
|
|
// Placeholder: return zeros with expected shape
|
|
// Expected output: [batch_size, seq_len=1, hidden_size]
|
|
let batch_size = input.dim(0)?;
|
|
let output = Tensor::zeros((batch_size, 1, self.hidden_size), DType::F32, &self.device)?;
|
|
Ok(output)
|
|
}
|
|
|
|
/// Get weight dtypes
|
|
pub fn get_weight_dtypes(&self) -> HashMap<String, DType> {
|
|
let mut dtypes = HashMap::new();
|
|
for (name, quantized) in &self.quantized_weights {
|
|
dtypes.insert(name.clone(), quantized.data.dtype());
|
|
}
|
|
dtypes
|
|
}
|
|
|
|
/// Get weight names
|
|
pub fn get_weight_names(&self) -> Vec<String> {
|
|
let mut names: Vec<String> = self.quantized_weights.keys().cloned().collect();
|
|
names.sort();
|
|
names
|
|
}
|
|
|
|
/// Get quantized weight
|
|
pub fn get_quantized_weight(&self, name: &str) -> Result<&QuantizedTensor, MLError> {
|
|
self.quantized_weights
|
|
.get(name)
|
|
.ok_or_else(|| MLError::ModelError(format!("Weight not found: {}", name)))
|
|
}
|
|
|
|
/// Dequantize weight
|
|
pub fn dequantize_weight(&self, name: &str) -> Result<Tensor, MLError> {
|
|
let quantized = self.get_quantized_weight(name)?;
|
|
Self::dequantize_u8_tensor(&quantized.data, quantized.scale, quantized.zero_point)
|
|
}
|
|
|
|
/// Dequantize U8 tensor to F32
|
|
fn dequantize_u8_tensor(
|
|
u8_tensor: &Tensor,
|
|
scale: f32,
|
|
zero_point: i8,
|
|
) -> Result<Tensor, MLError> {
|
|
// Dequantize: x = scale * (q - zero_point)
|
|
let zero_point_f32 = zero_point as f32;
|
|
let device = u8_tensor.device();
|
|
|
|
// Convert U8 to F32
|
|
let f32_tensor = u8_tensor
|
|
.to_dtype(DType::F32)
|
|
.map_err(|e| MLError::ModelError(format!("Failed to convert U8 to F32: {}", e)))?;
|
|
|
|
// Convert scalars to tensors for operations
|
|
let zero_point_tensor = Tensor::new(&[zero_point_f32], device)?;
|
|
let scale_tensor = Tensor::new(&[scale], device)?;
|
|
|
|
// Subtract zero point
|
|
let shifted = f32_tensor.broadcast_sub(&zero_point_tensor)?;
|
|
|
|
// Multiply by scale
|
|
let dequantized = shifted.broadcast_mul(&scale_tensor)?;
|
|
|
|
Ok(dequantized)
|
|
}
|
|
|
|
/// Calculate total memory usage in bytes
|
|
pub fn memory_bytes(&self) -> usize {
|
|
let mut total = 0;
|
|
|
|
for quantized in self.quantized_weights.values() {
|
|
total += quantized.memory_bytes();
|
|
}
|
|
|
|
// Add overhead for metadata (scales, zero_points)
|
|
let num_tensors = self.quantized_weights.len();
|
|
total += num_tensors * (size_of::<f32>() + size_of::<i8>());
|
|
|
|
total
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::len_zero)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[cfg_attr(not(feature = "cuda"), ignore)]
|
|
#[test]
|
|
fn test_quantized_vsn_creation() -> Result<(), MLError> {
|
|
let device = Device::new_cuda(0).expect("CUDA required");
|
|
let varmap = VarMap::new();
|
|
let vs = VarBuilder::from_varmap(&varmap, candle_core::DType::BF16, &device);
|
|
|
|
let vsn = VariableSelectionNetwork::new(5, 32, vs.pp("test"))?;
|
|
|
|
let config = QuantizationConfig {
|
|
quant_type: QuantizationType::Int8,
|
|
symmetric: true,
|
|
per_channel: true,
|
|
calibration_samples: Some(100),
|
|
};
|
|
|
|
let quantized_vsn =
|
|
QuantizedVariableSelectionNetwork::from_f32_model(&vsn, config, device)?;
|
|
|
|
assert!(quantized_vsn.quantized_weights.len() > 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg_attr(not(feature = "cuda"), ignore)]
|
|
#[test]
|
|
fn test_u8_conversion() -> Result<(), MLError> {
|
|
let device = Device::new_cuda(0).expect("CUDA required");
|
|
|
|
// Create test tensor
|
|
let data = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0];
|
|
let tensor = Tensor::from_slice(&data, (2, 3), &device)?;
|
|
|
|
// Convert to U8
|
|
let scale = 0.1_f32;
|
|
let zero_point = 127_i8; // Use 127 instead of 128 (valid i8 range is -128 to 127)
|
|
let u8_tensor =
|
|
QuantizedVariableSelectionNetwork::convert_to_u8_dtype(&tensor, scale, zero_point)?;
|
|
|
|
assert_eq!(u8_tensor.dtype(), DType::U8);
|
|
assert_eq!(u8_tensor.dims(), &[2, 3]);
|
|
|
|
// Dequantize
|
|
let f32_tensor =
|
|
QuantizedVariableSelectionNetwork::dequantize_u8_tensor(&u8_tensor, scale, zero_point)?;
|
|
|
|
assert_eq!(f32_tensor.dtype(), DType::F32);
|
|
assert_eq!(f32_tensor.dims(), &[2, 3]);
|
|
|
|
Ok(())
|
|
}
|
|
}
|