mirror of
https://github.com/rohitg00/ai-engineering-from-scratch.git
synced 2026-10-02 01:54:39 +08:00
Merge pull request #181 from rohitg00/feat/rust-coverage-pass-1
Rust coverage pass 1: dev env, edge inference, audio, INT8 quant
This commit is contained in:
+7
-3
@@ -25,6 +25,7 @@
|
||||
"has_quiz": true,
|
||||
"has_notebook": false,
|
||||
"code_files": [
|
||||
"main.rs",
|
||||
"verify.py"
|
||||
],
|
||||
"outputs": [
|
||||
@@ -2134,7 +2135,8 @@
|
||||
"has_quiz": true,
|
||||
"has_notebook": false,
|
||||
"code_files": [
|
||||
"main.py"
|
||||
"main.py",
|
||||
"main.rs"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
@@ -3690,7 +3692,8 @@
|
||||
"has_quiz": false,
|
||||
"has_notebook": true,
|
||||
"code_files": [
|
||||
"main.py"
|
||||
"main.py",
|
||||
"main.rs"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
@@ -5438,7 +5441,8 @@
|
||||
"has_quiz": true,
|
||||
"has_notebook": false,
|
||||
"code_files": [
|
||||
"main.py"
|
||||
"main.py",
|
||||
"main.rs"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Lesson: Dev Environment (phase 00 / lesson 01)
|
||||
// Topic: verify that the four-layer toolchain (system, package managers, runtimes, libs)
|
||||
// is reachable from a Rust binary. Spawns each tool with `--version`, captures stdout,
|
||||
// reports PASS/FAIL plus the parsed version string. Stdlib only.
|
||||
// Refs:
|
||||
// https://doc.rust-lang.org/std/process/struct.Command.html
|
||||
// https://doc.rust-lang.org/std/process/struct.Output.html
|
||||
// https://doc.rust-lang.org/book/ch12-00-an-io-project.html
|
||||
// Build: rustc --edition 2021 code/main.rs -o /tmp/lesson_dev_env && /tmp/lesson_dev_env
|
||||
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
struct Check {
|
||||
name: &'static str,
|
||||
program: &'static str,
|
||||
args: &'static [&'static str],
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
const CHECKS: &[Check] = &[
|
||||
Check { name: "Git", program: "git", args: &["--version"], optional: false },
|
||||
Check { name: "Python 3.10+", program: "python3", args: &["--version"], optional: false },
|
||||
Check { name: "Node.js", program: "node", args: &["--version"], optional: false },
|
||||
Check { name: "Rust (rustc)", program: "rustc", args: &["--version"], optional: false },
|
||||
Check { name: "Cargo", program: "cargo", args: &["--version"], optional: false },
|
||||
Check { name: "uv (Python)", program: "uv", args: &["--version"], optional: true },
|
||||
Check { name: "pnpm", program: "pnpm", args: &["--version"], optional: true },
|
||||
Check { name: "Julia", program: "julia", args: &["--version"], optional: true },
|
||||
];
|
||||
|
||||
fn run_check(check: &Check) -> Result<String, String> {
|
||||
let output = Command::new(check.program)
|
||||
.args(check.args)
|
||||
.output()
|
||||
.map_err(|e| format!("{}: {}", check.program, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!("exit code {:?}", output.status.code()));
|
||||
}
|
||||
|
||||
let combined = if !output.stdout.is_empty() {
|
||||
&output.stdout
|
||||
} else {
|
||||
&output.stderr
|
||||
};
|
||||
|
||||
let raw = String::from_utf8_lossy(combined);
|
||||
let line = raw.lines().next().unwrap_or("").trim().to_string();
|
||||
if line.is_empty() {
|
||||
Err("empty version output".to_string())
|
||||
} else {
|
||||
Ok(line)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_minor_python(version_line: &str) -> Option<(u32, u32)> {
|
||||
let trimmed = version_line.trim_start_matches("Python").trim();
|
||||
let mut parts = trimmed.split('.');
|
||||
let major: u32 = parts.next()?.parse().ok()?;
|
||||
let minor: u32 = parts.next()?.parse().ok()?;
|
||||
Some((major, minor))
|
||||
}
|
||||
|
||||
fn print_header() {
|
||||
println!();
|
||||
println!("=== AI Engineering from Scratch — Environment Check (Rust) ===");
|
||||
println!();
|
||||
println!("Layer 1 (system) -> Layer 2 (package managers) -> Layer 3 (runtimes) -> Layer 4 (libs)");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
print_header();
|
||||
|
||||
let mut required_pass = 0u32;
|
||||
let mut required_total = 0u32;
|
||||
let mut optional_pass = 0u32;
|
||||
let mut optional_total = 0u32;
|
||||
|
||||
let mut python_ok = true;
|
||||
|
||||
println!("Required tools:");
|
||||
for check in CHECKS.iter().filter(|c| !c.optional) {
|
||||
required_total += 1;
|
||||
match run_check(check) {
|
||||
Ok(version) => {
|
||||
if check.name.starts_with("Python") {
|
||||
match parse_minor_python(&version) {
|
||||
Some((major, minor)) if (major, minor) >= (3, 10) => {}
|
||||
_ => {
|
||||
println!(" [FAIL] {:<14} {} (need parseable Python 3.10+)", check.name, version);
|
||||
python_ok = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
required_pass += 1;
|
||||
println!(" [PASS] {:<14} {}", check.name, version);
|
||||
}
|
||||
Err(why) => {
|
||||
println!(" [FAIL] {:<14} {}", check.name, why);
|
||||
if check.name.starts_with("Python") {
|
||||
python_ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Optional tools:");
|
||||
for check in CHECKS.iter().filter(|c| c.optional) {
|
||||
optional_total += 1;
|
||||
match run_check(check) {
|
||||
Ok(version) => {
|
||||
optional_pass += 1;
|
||||
println!(" [PASS] {:<14} {}", check.name, version);
|
||||
}
|
||||
Err(_) => {
|
||||
println!(" [skip] {:<14} not installed", check.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Summary: {}/{} required, {}/{} optional",
|
||||
required_pass, required_total, optional_pass, optional_total);
|
||||
|
||||
if required_pass == required_total && python_ok {
|
||||
println!();
|
||||
println!("Environment is ready. Start with Phase 1.");
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
println!();
|
||||
println!("Fix the failed checks above, then run this again.");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Lesson: Real-Time Vision Edge Deployment (phase 04 / lesson 15)
|
||||
// Topic: edge inference loop in Rust. Builds a tiny depthwise-separable conv block
|
||||
// (the MobileNet primitive), runs it over a 160x160x3 input tensor, and reports
|
||||
// p50/p95/p99 latency the way an on-device profiler would. Stdlib only.
|
||||
// Refs:
|
||||
// https://doc.rust-lang.org/std/time/struct.Instant.html
|
||||
// https://arxiv.org/abs/1704.04861 (MobileNetV1: depthwise separable convolutions)
|
||||
// https://pytorch.org/docs/stable/quantization.html (edge measurement discipline)
|
||||
// Build: rustc --edition 2021 -O code/main.rs -o /tmp/lesson_edge && /tmp/lesson_edge
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
const H: usize = 160;
|
||||
const W: usize = 160;
|
||||
const C_IN: usize = 3;
|
||||
const C_OUT: usize = 16;
|
||||
const K: usize = 3;
|
||||
const WARMUP: usize = 3;
|
||||
const ITERS: usize = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Tensor {
|
||||
data: Vec<f32>,
|
||||
h: usize,
|
||||
w: usize,
|
||||
c: usize,
|
||||
}
|
||||
|
||||
impl Tensor {
|
||||
fn zeros(h: usize, w: usize, c: usize) -> Self {
|
||||
Self { data: vec![0.0; h * w * c], h, w, c }
|
||||
}
|
||||
|
||||
fn idx(&self, y: usize, x: usize, c: usize) -> usize {
|
||||
(y * self.w + x) * self.c + c
|
||||
}
|
||||
}
|
||||
|
||||
// Cheap deterministic PRNG. Avoids pulling in rand for a stdlib-only lesson.
|
||||
fn lcg(seed: &mut u64) -> f32 {
|
||||
*seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let bits = (*seed >> 33) as u32;
|
||||
(bits as f32 / u32::MAX as f32) * 2.0 - 1.0
|
||||
}
|
||||
|
||||
fn fill_random(t: &mut Tensor, seed: &mut u64) {
|
||||
for v in t.data.iter_mut() {
|
||||
*v = lcg(seed) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Depthwise conv: one 3x3 kernel per input channel, no cross-channel mixing.
|
||||
// This is the part MobileNet uses to cut FLOPs by ~9x vs a dense conv.
|
||||
fn depthwise_conv(input: &Tensor, weights: &[f32]) -> Tensor {
|
||||
let mut out = Tensor::zeros(input.h, input.w, input.c);
|
||||
let pad = K / 2;
|
||||
for y in 0..input.h {
|
||||
for x in 0..input.w {
|
||||
for c in 0..input.c {
|
||||
let mut acc = 0.0;
|
||||
for ky in 0..K {
|
||||
for kx in 0..K {
|
||||
let iy = y as isize + ky as isize - pad as isize;
|
||||
let ix = x as isize + kx as isize - pad as isize;
|
||||
if iy < 0 || ix < 0 || iy >= input.h as isize || ix >= input.w as isize {
|
||||
continue;
|
||||
}
|
||||
let pixel = input.data[input.idx(iy as usize, ix as usize, c)];
|
||||
let w_idx = c * K * K + ky * K + kx;
|
||||
acc += pixel * weights[w_idx];
|
||||
}
|
||||
}
|
||||
let oi = out.idx(y, x, c);
|
||||
out.data[oi] = acc.max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Pointwise 1x1 conv: mixes channels. Together with the depthwise above this is
|
||||
// one MobileNet block: ~8-9x cheaper than a full HxWxC_in x C_out 3x3 dense conv.
|
||||
fn pointwise_conv(input: &Tensor, weights: &[f32], c_out: usize) -> Tensor {
|
||||
let mut out = Tensor::zeros(input.h, input.w, c_out);
|
||||
for y in 0..input.h {
|
||||
for x in 0..input.w {
|
||||
for co in 0..c_out {
|
||||
let mut acc = 0.0;
|
||||
for ci in 0..input.c {
|
||||
let pixel = input.data[input.idx(y, x, ci)];
|
||||
let w_idx = co * input.c + ci;
|
||||
acc += pixel * weights[w_idx];
|
||||
}
|
||||
let oi = out.idx(y, x, co);
|
||||
out.data[oi] = acc.max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn forward(input: &Tensor, dw_w: &[f32], pw_w: &[f32]) -> Tensor {
|
||||
let dw = depthwise_conv(input, dw_w);
|
||||
pointwise_conv(&dw, pw_w, C_OUT)
|
||||
}
|
||||
|
||||
fn flops_per_pass() -> u64 {
|
||||
let dw = (H * W * C_IN * K * K * 2) as u64;
|
||||
let pw = (H * W * C_IN * C_OUT * 2) as u64;
|
||||
dw + pw
|
||||
}
|
||||
|
||||
fn percentile(sorted_ms: &[f64], pct: f64) -> f64 {
|
||||
if sorted_ms.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let idx = ((sorted_ms.len() as f64 - 1.0) * pct).round() as usize;
|
||||
sorted_ms[idx]
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut seed: u64 = 0xa1b2_c3d4_e5f6_0708;
|
||||
|
||||
let mut input = Tensor::zeros(H, W, C_IN);
|
||||
fill_random(&mut input, &mut seed);
|
||||
|
||||
let mut dw_weights = vec![0.0f32; C_IN * K * K];
|
||||
let mut pw_weights = vec![0.0f32; C_OUT * C_IN];
|
||||
for w in dw_weights.iter_mut() { *w = lcg(&mut seed) * 0.1; }
|
||||
for w in pw_weights.iter_mut() { *w = lcg(&mut seed) * 0.1; }
|
||||
|
||||
println!();
|
||||
println!("=== Edge inference benchmark (Rust, single thread) ===");
|
||||
println!();
|
||||
println!("Model : depthwise 3x3 + pointwise 1x1 (one MobileNet block)");
|
||||
println!("Input shape: {}x{}x{}", H, W, C_IN);
|
||||
println!("Output ch : {}", C_OUT);
|
||||
let flops = flops_per_pass();
|
||||
println!("FLOPs/pass : {:.2} M", flops as f64 / 1e6);
|
||||
println!();
|
||||
|
||||
println!("Warming up ({} iters, ignored)...", WARMUP);
|
||||
for _ in 0..WARMUP {
|
||||
let _ = forward(&input, &dw_weights, &pw_weights);
|
||||
}
|
||||
|
||||
println!("Measuring ({} iters)...", ITERS);
|
||||
let mut times_ms = Vec::with_capacity(ITERS);
|
||||
for _ in 0..ITERS {
|
||||
let t0 = Instant::now();
|
||||
let out = forward(&input, &dw_weights, &pw_weights);
|
||||
let dt = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
times_ms.push(dt);
|
||||
std::hint::black_box(out);
|
||||
}
|
||||
|
||||
let mut sorted = times_ms.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let p50 = percentile(&sorted, 0.50);
|
||||
let p95 = percentile(&sorted, 0.95);
|
||||
let p99 = percentile(&sorted, 0.99);
|
||||
let mean: f64 = times_ms.iter().sum::<f64>() / times_ms.len() as f64;
|
||||
let min = sorted[0];
|
||||
let max = *sorted.last().unwrap();
|
||||
|
||||
println!();
|
||||
println!("Latency (ms):");
|
||||
println!(" p50 {:>8.2}", p50);
|
||||
println!(" p95 {:>8.2}", p95);
|
||||
println!(" p99 {:>8.2}", p99);
|
||||
println!(" mean {:>8.2}", mean);
|
||||
println!(" min {:>8.2}", min);
|
||||
println!(" max {:>8.2}", max);
|
||||
|
||||
let throughput_fps = 1000.0 / p50;
|
||||
let gflops_s = (flops as f64) / (p50 / 1000.0) / 1e9;
|
||||
println!();
|
||||
println!("Throughput (from p50):");
|
||||
println!(" {:>5.1} fps {:>5.2} GFLOPs/s", throughput_fps, gflops_s);
|
||||
|
||||
println!();
|
||||
println!("Edge measurement discipline (also enforced here):");
|
||||
println!(" - {} warmup passes ignored to avoid cold-cache bias", WARMUP);
|
||||
println!(" - fixed input resolution (production resolution must match)");
|
||||
println!(" - p50 reported alongside p99 so tail latency is visible");
|
||||
println!();
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Lesson: Real-Time Audio Processing (phase 06 / lesson 11)
|
||||
// Topic: stream a 16 kHz mono sine wave through 20 ms frames, apply a gain stage
|
||||
// and a 9-tap low-pass FIR filter, measure per-frame and aggregate throughput.
|
||||
// This is the inner loop every voice agent runs under VAD/ASR/TTS.
|
||||
// Refs:
|
||||
// https://doc.rust-lang.org/std/time/struct.Instant.html
|
||||
// https://en.wikipedia.org/wiki/Finite_impulse_response
|
||||
// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_processing (20 ms frame convention)
|
||||
// Build: rustc --edition 2021 -O code/main.rs -o /tmp/lesson_audio && /tmp/lesson_audio
|
||||
|
||||
use std::f32::consts::PI;
|
||||
use std::time::Instant;
|
||||
|
||||
const SAMPLE_RATE: u32 = 16_000;
|
||||
const FRAME_MS: u32 = 20;
|
||||
const FRAME_LEN: usize = (SAMPLE_RATE / 1000 * FRAME_MS) as usize; // 320 samples
|
||||
const TONE_HZ: f32 = 440.0;
|
||||
const TOTAL_SECONDS: f32 = 2.0;
|
||||
const GAIN_DB: f32 = -3.0;
|
||||
|
||||
// 9-tap symmetric low-pass FIR. Hand-tuned, sum ~= 1.0 so DC is preserved.
|
||||
const FIR_TAPS: [f32; 9] = [
|
||||
0.02, 0.06, 0.12, 0.18, 0.24, 0.18, 0.12, 0.06, 0.02,
|
||||
];
|
||||
|
||||
fn db_to_linear(db: f32) -> f32 {
|
||||
10f32.powf(db / 20.0)
|
||||
}
|
||||
|
||||
fn synth_sine_frame(start_sample: u64, freq_hz: f32, sr: u32) -> Vec<f32> {
|
||||
let mut frame = Vec::with_capacity(FRAME_LEN);
|
||||
let two_pi_f_over_sr = 2.0 * PI * freq_hz / sr as f32;
|
||||
for n in 0..FRAME_LEN {
|
||||
let t = (start_sample + n as u64) as f32;
|
||||
frame.push((two_pi_f_over_sr * t).sin());
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
fn apply_gain(frame: &mut [f32], gain_lin: f32) {
|
||||
for s in frame.iter_mut() {
|
||||
*s *= gain_lin;
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming FIR. `state` carries the last (taps-1) samples across frame boundaries
|
||||
// so the filter sees a continuous signal, not 20 ms islands with edge artefacts.
|
||||
fn fir_streaming(frame: &mut [f32], taps: &[f32], state: &mut Vec<f32>) {
|
||||
let order = taps.len();
|
||||
let mut buf = Vec::with_capacity(state.len() + frame.len());
|
||||
buf.extend_from_slice(state);
|
||||
buf.extend_from_slice(frame);
|
||||
|
||||
for n in 0..frame.len() {
|
||||
let mut acc = 0.0;
|
||||
for k in 0..order {
|
||||
acc += taps[k] * buf[n + order - 1 - k];
|
||||
}
|
||||
frame[n] = acc;
|
||||
}
|
||||
|
||||
let keep = order - 1;
|
||||
state.clear();
|
||||
state.extend_from_slice(&buf[buf.len() - keep..]);
|
||||
}
|
||||
|
||||
fn rms(frame: &[f32]) -> f32 {
|
||||
let sum_sq: f32 = frame.iter().map(|x| x * x).sum();
|
||||
(sum_sq / frame.len() as f32).sqrt()
|
||||
}
|
||||
|
||||
fn rms_dbfs(frame: &[f32]) -> f32 {
|
||||
let r = rms(frame).max(1e-10);
|
||||
20.0 * r.log10()
|
||||
}
|
||||
|
||||
fn percentile(sorted_us: &[f64], pct: f64) -> f64 {
|
||||
if sorted_us.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let idx = ((sorted_us.len() as f64 - 1.0) * pct).round() as usize;
|
||||
sorted_us[idx]
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let total_samples = (SAMPLE_RATE as f32 * TOTAL_SECONDS) as u64;
|
||||
let total_frames = (total_samples as usize) / FRAME_LEN;
|
||||
let gain_lin = db_to_linear(GAIN_DB);
|
||||
|
||||
println!();
|
||||
println!("=== Real-time audio benchmark (Rust, single thread) ===");
|
||||
println!();
|
||||
println!("Sample rate : {} Hz", SAMPLE_RATE);
|
||||
println!("Frame size : {} ms ({} samples)", FRAME_MS, FRAME_LEN);
|
||||
println!("Stream length: {:.1} s ({} frames)", TOTAL_SECONDS, total_frames);
|
||||
println!("Tone : {} Hz sine", TONE_HZ);
|
||||
println!("Gain stage : {:+.1} dB", GAIN_DB);
|
||||
println!("FIR : {}-tap symmetric low-pass", FIR_TAPS.len());
|
||||
println!();
|
||||
|
||||
let mut fir_state = vec![0.0f32; FIR_TAPS.len() - 1];
|
||||
let mut per_frame_us: Vec<f64> = Vec::with_capacity(total_frames);
|
||||
let mut rms_in_db = 0.0f32;
|
||||
let mut rms_out_db = 0.0f32;
|
||||
|
||||
let wall = Instant::now();
|
||||
for f in 0..total_frames {
|
||||
let start_sample = (f * FRAME_LEN) as u64;
|
||||
let mut frame = synth_sine_frame(start_sample, TONE_HZ, SAMPLE_RATE);
|
||||
|
||||
let t_frame = Instant::now();
|
||||
if f == 0 { rms_in_db = rms_dbfs(&frame); }
|
||||
|
||||
apply_gain(&mut frame, gain_lin);
|
||||
fir_streaming(&mut frame, &FIR_TAPS, &mut fir_state);
|
||||
|
||||
if f == 0 { rms_out_db = rms_dbfs(&frame); }
|
||||
per_frame_us.push(t_frame.elapsed().as_secs_f64() * 1e6);
|
||||
}
|
||||
let wall_ms = wall.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
let mut sorted = per_frame_us.clone();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let p50 = percentile(&sorted, 0.50);
|
||||
let p95 = percentile(&sorted, 0.95);
|
||||
let p99 = percentile(&sorted, 0.99);
|
||||
let mean = per_frame_us.iter().sum::<f64>() / per_frame_us.len() as f64;
|
||||
|
||||
let budget_us = (FRAME_MS as f64) * 1000.0;
|
||||
let headroom = budget_us / p99.max(1e-9);
|
||||
|
||||
println!("Per-frame latency (us):");
|
||||
println!(" p50 {:>9.2}", p50);
|
||||
println!(" p95 {:>9.2}", p95);
|
||||
println!(" p99 {:>9.2}", p99);
|
||||
println!(" mean {:>9.2}", mean);
|
||||
println!();
|
||||
println!("Aggregate:");
|
||||
println!(" wall time {:>8.2} ms", wall_ms);
|
||||
println!(" realtime budget {:>8.2} ms ({} frames * {} ms)", total_frames as f64 * FRAME_MS as f64, total_frames, FRAME_MS);
|
||||
println!(" realtime factor {:>8.1}x (wall/budget; lower is faster)", wall_ms / (total_frames as f64 * FRAME_MS as f64));
|
||||
println!(" headroom per p99 {:>8.1}x (budget / p99)", headroom);
|
||||
println!();
|
||||
println!("Signal levels (frame 0):");
|
||||
println!(" RMS in {:>7.2} dBFS", rms_in_db);
|
||||
println!(" RMS out {:>7.2} dBFS (after {:+.1} dB gain + FIR)", rms_out_db, GAIN_DB);
|
||||
println!();
|
||||
|
||||
if headroom >= 50.0 {
|
||||
println!("Verdict: huge headroom. VAD + STT + LLM + TTS all fit in the 20 ms slot.");
|
||||
} else if headroom >= 5.0 {
|
||||
println!("Verdict: comfortable headroom. Streaming pipeline will fit.");
|
||||
} else {
|
||||
println!("Verdict: too slow. Pipeline will drop frames at this DSP cost.");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Lesson: Quantization — INT8 / GPTQ / AWQ / GGUF (phase 10 / lesson 11)
|
||||
// Topic: symmetric INT8 quantization of an FP32 weight vector. Computes scale
|
||||
// from abs-max, rounds + clips to [-127, 127], dequantizes, reports MSE,
|
||||
// max abs error, SNR, cosine similarity, and a bit-width sweep (8 / 4 / 2 bit).
|
||||
// Refs:
|
||||
// https://pytorch.org/docs/stable/quantization.html
|
||||
// https://leimao.github.io/article/Neural-Networks-Quantization/
|
||||
// https://arxiv.org/abs/2210.17323 (GPTQ)
|
||||
// https://arxiv.org/abs/2306.00978 (AWQ)
|
||||
// Build: rustc --edition 2021 -O code/main.rs -o /tmp/lesson_quant && /tmp/lesson_quant
|
||||
|
||||
use std::f64;
|
||||
|
||||
fn lcg(seed: &mut u64) -> f64 {
|
||||
*seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let bits = (*seed >> 11) as u64;
|
||||
let unit = bits as f64 / (1u64 << 53) as f64;
|
||||
unit * 2.0 - 1.0
|
||||
}
|
||||
|
||||
// Box-Muller via the LCG, so we generate normal-ish floats without external crates.
|
||||
fn randn(seed: &mut u64) -> f64 {
|
||||
let u1 = (lcg(seed) + 1.0) / 2.0;
|
||||
let u2 = (lcg(seed) + 1.0) / 2.0;
|
||||
let u1 = u1.max(1e-12);
|
||||
let r = (-2.0 * u1.ln()).sqrt();
|
||||
r * (2.0 * std::f64::consts::PI * u2).cos()
|
||||
}
|
||||
|
||||
struct QuantResult {
|
||||
qmin: i32,
|
||||
qmax: i32,
|
||||
scale: f64,
|
||||
quantized: Vec<i32>,
|
||||
reconstructed: Vec<f64>,
|
||||
}
|
||||
|
||||
fn quantize_symmetric(weights: &[f64], num_bits: u32) -> QuantResult {
|
||||
let qmax = (1i32 << (num_bits - 1)) - 1;
|
||||
let qmin = -qmax;
|
||||
|
||||
let abs_max = weights.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
|
||||
let scale = if abs_max == 0.0 { 1.0 } else { abs_max / qmax as f64 };
|
||||
|
||||
let mut quantized = Vec::with_capacity(weights.len());
|
||||
let mut reconstructed = Vec::with_capacity(weights.len());
|
||||
for &w in weights {
|
||||
let q = (w / scale).round() as i32;
|
||||
let q = q.max(qmin).min(qmax);
|
||||
quantized.push(q);
|
||||
reconstructed.push(q as f64 * scale);
|
||||
}
|
||||
|
||||
QuantResult { qmin, qmax, scale, quantized, reconstructed }
|
||||
}
|
||||
|
||||
struct ErrorReport {
|
||||
mse: f64,
|
||||
rmse: f64,
|
||||
max_abs_error: f64,
|
||||
snr_db: f64,
|
||||
cosine: f64,
|
||||
}
|
||||
|
||||
fn error_report(original: &[f64], reconstructed: &[f64]) -> ErrorReport {
|
||||
let n = original.len() as f64;
|
||||
let mut sum_sq_err = 0.0f64;
|
||||
let mut max_abs = 0.0f64;
|
||||
let mut signal_power = 0.0f64;
|
||||
let mut dot = 0.0f64;
|
||||
let mut norm_a = 0.0f64;
|
||||
let mut norm_b = 0.0f64;
|
||||
|
||||
for (a, b) in original.iter().zip(reconstructed.iter()) {
|
||||
let diff = a - b;
|
||||
sum_sq_err += diff * diff;
|
||||
max_abs = max_abs.max(diff.abs());
|
||||
signal_power += a * a;
|
||||
dot += a * b;
|
||||
norm_a += a * a;
|
||||
norm_b += b * b;
|
||||
}
|
||||
|
||||
let mse = sum_sq_err / n;
|
||||
let rmse = mse.sqrt();
|
||||
let snr_db = if mse > 0.0 {
|
||||
10.0 * (signal_power / n / mse).log10()
|
||||
} else {
|
||||
f64::INFINITY
|
||||
};
|
||||
let cosine = if norm_a > 0.0 && norm_b > 0.0 {
|
||||
dot / (norm_a.sqrt() * norm_b.sqrt())
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
ErrorReport { mse, rmse, max_abs_error: max_abs, snr_db, cosine }
|
||||
}
|
||||
|
||||
fn print_quant_summary(label: &str, weights: &[f64], r: &QuantResult, err: &ErrorReport) {
|
||||
println!("[{}]", label);
|
||||
println!(" range [qmin, qmax] {} .. {}", r.qmin, r.qmax);
|
||||
println!(" scale (FP32 step) {:.8}", r.scale);
|
||||
println!(" sample weights (10) {:?}", &weights[..10.min(weights.len())]
|
||||
.iter().map(|w| format!("{:+.4}", w)).collect::<Vec<_>>());
|
||||
println!(" quantized codes (10) {:?}", &r.quantized[..10.min(r.quantized.len())]);
|
||||
println!(" dequantized (10) {:?}", &r.reconstructed[..10.min(r.reconstructed.len())]
|
||||
.iter().map(|w| format!("{:+.4}", w)).collect::<Vec<_>>());
|
||||
println!();
|
||||
println!(" mse {:.10}", err.mse);
|
||||
println!(" rmse {:.10}", err.rmse);
|
||||
println!(" max |error| {:.10}", err.max_abs_error);
|
||||
println!(" snr {:.2} dB", err.snr_db);
|
||||
println!(" cosine similarity {:.10}", err.cosine);
|
||||
println!();
|
||||
}
|
||||
|
||||
fn fmt_bytes(b: u64) -> String {
|
||||
let kb = b as f64 / 1024.0;
|
||||
if kb < 1024.0 { format!("{:.2} KB", kb) } else { format!("{:.2} MB", kb / 1024.0) }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut seed: u64 = 42;
|
||||
|
||||
let n = 8192;
|
||||
let mut weights: Vec<f64> = (0..n).map(|_| randn(&mut seed) * 0.02).collect();
|
||||
|
||||
weights[0] *= 25.0;
|
||||
weights[123] *= 15.0;
|
||||
weights[2048] *= 10.0;
|
||||
|
||||
let stats = {
|
||||
let abs_vals: Vec<f64> = weights.iter().map(|x| x.abs()).collect();
|
||||
let max = abs_vals.iter().fold(0.0f64, |a, &b| a.max(b));
|
||||
let mean: f64 = abs_vals.iter().sum::<f64>() / abs_vals.len() as f64;
|
||||
let var: f64 = abs_vals.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / abs_vals.len() as f64;
|
||||
(max, mean, var.sqrt())
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("=== INT8 quantization (Rust, stdlib only) ===");
|
||||
println!();
|
||||
println!("Tensor : 1D weight vector, n = {}", n);
|
||||
println!("Distribution : Normal(0, 0.02) with 3 outlier weights");
|
||||
println!(" max |w| {:.6}", stats.0);
|
||||
println!(" mean |w| {:.6}", stats.1);
|
||||
println!(" std |w| {:.6}", stats.2);
|
||||
println!();
|
||||
|
||||
let r8 = quantize_symmetric(&weights, 8);
|
||||
let err8 = error_report(&weights, &r8.reconstructed);
|
||||
print_quant_summary("INT8 symmetric per-tensor", &weights, &r8, &err8);
|
||||
|
||||
println!("--- Bit-width sweep (symmetric per-tensor) ---");
|
||||
println!(" {:>5} {:>10} {:>14} {:>10} {:>12} {:>10}",
|
||||
"bits", "levels", "mse", "snr_db", "max |err|", "ratio_vs_fp32");
|
||||
for bits in [16u32, 8, 4, 2] {
|
||||
let r = quantize_symmetric(&weights, bits);
|
||||
let er = error_report(&weights, &r.reconstructed);
|
||||
let ratio = 32.0 / bits as f64;
|
||||
let levels = (r.qmax - r.qmin + 1) as u64;
|
||||
println!(" {:>5} {:>10} {:>14.10} {:>10.2} {:>12.6} {:>9.1}x",
|
||||
bits, levels, er.mse, er.snr_db, er.max_abs_error, ratio);
|
||||
}
|
||||
println!();
|
||||
|
||||
let fp32_bytes = (n * 4) as u64;
|
||||
let int8_bytes = (n * 1) as u64 + 8;
|
||||
let int4_bytes = ((n + 1) / 2) as u64 + 8;
|
||||
println!("--- Memory footprint ---");
|
||||
println!(" FP32 weights {}", fmt_bytes(fp32_bytes));
|
||||
println!(" INT8 + scale {} ({:.1}x smaller)", fmt_bytes(int8_bytes), fp32_bytes as f64 / int8_bytes as f64);
|
||||
println!(" INT4 + scale {} ({:.1}x smaller)", fmt_bytes(int4_bytes), fp32_bytes as f64 / int4_bytes as f64);
|
||||
println!();
|
||||
|
||||
println!("Takeaway:");
|
||||
println!(" - INT8 keeps SNR well above 30 dB for normal weight distributions.");
|
||||
println!(" - Outliers dominate scale: 3 outliers in {} weights inflate scale and ", n);
|
||||
println!(" waste precision on the rest. Per-channel (or GPTQ/AWQ) helps.");
|
||||
println!();
|
||||
}
|
||||
Reference in New Issue
Block a user