Skip to main content

Int8Tier

Enum Int8Tier 

Source
pub enum Int8Tier {
    Scalar,
    Autovec,
    NeonSdot,
}
Expand description

An executable int8 dot-product route.

Every variant is exactly equal in i32 to Scalar on every input. NeonSdot exists only on aarch64 builds with the neon-dotprod feature and is dispatchable only where the CPU reports FEAT_DotProd at runtime.

Variants§

§

Scalar

Portable left-to-right checked-free scalar loop; the reference every tier must equal.

§

Autovec

Portable eight-lane loop, retained ONLY as an A/B datapoint: measured ~15x SLOWER than Scalar at m=1 on M4 Pro (NE-001) — the manual lane structure defeats LLVM’s autovectorizer, while the plain Scalar shape vectorizes to memory bandwidth. Never the dispatch default.

§

NeonSdot

Hand SDOT island (aarch64 + FEAT_DotProd), four 16-byte accumulator streams.

Implementations§

Source§

impl Int8Tier

Source

pub const fn as_str(self) -> &'static str

Stable machine-readable route name.

Examples found in repository?
examples/int8_shape_bench.rs (line 65)
62fn main() {
63    let tiers: Vec<Int8Tier> = Int8Tier::available();
64    println!("int8 shape bench — tiers available: {:?}", {
65        tiers.iter().map(|t| t.as_str()).collect::<Vec<_>>()
66    });
67    println!(
68        "interleaved rounds={ROUNDS} (+{WARMUP_ROUNDS} warmup); per-sample = mean over calls in one round; cv% over rounds"
69    );
70
71    for &m in &[1_usize, 16] {
72        println!(
73            "\n== m = {m} {} ==",
74            if m == 1 {
75                "(decode GEMV)"
76            } else {
77                "(seq-16 verify GEMM)"
78            }
79        );
80        for &(label, n, k) in SHAPES {
81            let calls: usize = (32 / m).max(2);
82            let weight = pseudo_random_f32(n * k, 0xbe0_0001 ^ (n as u64) << 20 ^ k as u64);
83            let x = pseudo_random_f32(m * k, 0xbe0_0002 ^ (m as u64) << 32 ^ k as u64);
84            let quantized = QuantizedMatrix::quantize(&weight, n, k);
85            let mut out = vec![0.0_f32; m * n];
86            let mut x_q = vec![0_i8; m * k];
87            let mut x_scales = vec![0.0_f32; m];
88
89            // One arm per route, all interleaved inside every round.
90            let mut f32_samples = Vec::with_capacity(ROUNDS);
91            let mut tier_samples: Vec<Vec<f64>> =
92                tiers.iter().map(|_| Vec::with_capacity(ROUNDS)).collect();
93
94            for round in 0..ROUNDS + WARMUP_ROUNDS {
95                // f32 arm
96                let start = Instant::now();
97                for _ in 0..calls {
98                    f32ref::linear(
99                        black_box(&x),
100                        black_box(&weight),
101                        None,
102                        m,
103                        k,
104                        n,
105                        black_box(&mut out),
106                    );
107                }
108                let f32_us = start.elapsed().as_secs_f64() * 1e6 / calls as f64;
109
110                // W8A8 arms, including dynamic activation quantization each call.
111                let mut this_round = Vec::with_capacity(tiers.len());
112                for &tier in &tiers {
113                    let start = Instant::now();
114                    for _ in 0..calls {
115                        for ((x_row, q_row), scale) in x
116                            .chunks_exact(k)
117                            .zip(x_q.chunks_exact_mut(k))
118                            .zip(x_scales.iter_mut())
119                        {
120                            *scale = quantize_row_q8(black_box(x_row), q_row);
121                        }
122                        linear_q8(
123                            black_box(&x_q),
124                            black_box(&x_scales),
125                            black_box(&quantized),
126                            None,
127                            m,
128                            black_box(&mut out),
129                            tier,
130                        );
131                    }
132                    this_round.push(start.elapsed().as_secs_f64() * 1e6 / calls as f64);
133                }
134
135                if round >= WARMUP_ROUNDS {
136                    f32_samples.push(f32_us);
137                    for (samples, sample) in tier_samples.iter_mut().zip(&this_round) {
138                        samples.push(*sample);
139                    }
140                }
141            }
142
143            let f32_stats = stats(&f32_samples);
144            // The f32 reference loops rows outermost, so it streams the weight matrix once per
145            // activation row (m times per call); the q8 kernel is weight-stationary and streams
146            // it exactly once per call. The column reports actual weight bytes moved per second.
147            let f32_bytes = (n * k * 4 * m) as f64;
148            println!(
149                "{label}  f32     {:9.1} us  cv {:4.1}%  ({:5.1} GB/s weight-stream)",
150                f32_stats.mean_us,
151                f32_stats.cv_percent,
152                f32_bytes / (f32_stats.mean_us * 1e-6) / 1e9,
153            );
154            for (tier, samples) in tiers.iter().zip(&tier_samples) {
155                let tier_stats = stats(samples);
156                let q8_bytes = (n * k) as f64;
157                let verdict = if tier_stats.cv_percent > 5.0 || f32_stats.cv_percent > 5.0 {
158                    "REFUSED (cv>5%)"
159                } else {
160                    ""
161                };
162                println!(
163                    "{label}  q8 {:9} {:9.1} us  cv {:4.1}%  ({:5.1} GB/s weight-stream)  x{:.2} vs f32 {verdict}",
164                    tier.as_str(),
165                    tier_stats.mean_us,
166                    tier_stats.cv_percent,
167                    q8_bytes / (tier_stats.mean_us * 1e-6) / 1e9,
168                    f32_stats.mean_us / tier_stats.mean_us,
169                );
170            }
171        }
172    }
173    println!(
174        "\nNOTE: ratios above compare routes inside this tree (self-comparison = maintenance),\nnever a pinned incumbent. cv%>5 rows are refused, not averaged."
175    );
176}
Source

pub fn available() -> Vec<Self>

Every tier this build can execute on the running silicon, scalar first.

Examples found in repository?
examples/int8_shape_bench.rs (line 63)
62fn main() {
63    let tiers: Vec<Int8Tier> = Int8Tier::available();
64    println!("int8 shape bench — tiers available: {:?}", {
65        tiers.iter().map(|t| t.as_str()).collect::<Vec<_>>()
66    });
67    println!(
68        "interleaved rounds={ROUNDS} (+{WARMUP_ROUNDS} warmup); per-sample = mean over calls in one round; cv% over rounds"
69    );
70
71    for &m in &[1_usize, 16] {
72        println!(
73            "\n== m = {m} {} ==",
74            if m == 1 {
75                "(decode GEMV)"
76            } else {
77                "(seq-16 verify GEMM)"
78            }
79        );
80        for &(label, n, k) in SHAPES {
81            let calls: usize = (32 / m).max(2);
82            let weight = pseudo_random_f32(n * k, 0xbe0_0001 ^ (n as u64) << 20 ^ k as u64);
83            let x = pseudo_random_f32(m * k, 0xbe0_0002 ^ (m as u64) << 32 ^ k as u64);
84            let quantized = QuantizedMatrix::quantize(&weight, n, k);
85            let mut out = vec![0.0_f32; m * n];
86            let mut x_q = vec![0_i8; m * k];
87            let mut x_scales = vec![0.0_f32; m];
88
89            // One arm per route, all interleaved inside every round.
90            let mut f32_samples = Vec::with_capacity(ROUNDS);
91            let mut tier_samples: Vec<Vec<f64>> =
92                tiers.iter().map(|_| Vec::with_capacity(ROUNDS)).collect();
93
94            for round in 0..ROUNDS + WARMUP_ROUNDS {
95                // f32 arm
96                let start = Instant::now();
97                for _ in 0..calls {
98                    f32ref::linear(
99                        black_box(&x),
100                        black_box(&weight),
101                        None,
102                        m,
103                        k,
104                        n,
105                        black_box(&mut out),
106                    );
107                }
108                let f32_us = start.elapsed().as_secs_f64() * 1e6 / calls as f64;
109
110                // W8A8 arms, including dynamic activation quantization each call.
111                let mut this_round = Vec::with_capacity(tiers.len());
112                for &tier in &tiers {
113                    let start = Instant::now();
114                    for _ in 0..calls {
115                        for ((x_row, q_row), scale) in x
116                            .chunks_exact(k)
117                            .zip(x_q.chunks_exact_mut(k))
118                            .zip(x_scales.iter_mut())
119                        {
120                            *scale = quantize_row_q8(black_box(x_row), q_row);
121                        }
122                        linear_q8(
123                            black_box(&x_q),
124                            black_box(&x_scales),
125                            black_box(&quantized),
126                            None,
127                            m,
128                            black_box(&mut out),
129                            tier,
130                        );
131                    }
132                    this_round.push(start.elapsed().as_secs_f64() * 1e6 / calls as f64);
133                }
134
135                if round >= WARMUP_ROUNDS {
136                    f32_samples.push(f32_us);
137                    for (samples, sample) in tier_samples.iter_mut().zip(&this_round) {
138                        samples.push(*sample);
139                    }
140                }
141            }
142
143            let f32_stats = stats(&f32_samples);
144            // The f32 reference loops rows outermost, so it streams the weight matrix once per
145            // activation row (m times per call); the q8 kernel is weight-stationary and streams
146            // it exactly once per call. The column reports actual weight bytes moved per second.
147            let f32_bytes = (n * k * 4 * m) as f64;
148            println!(
149                "{label}  f32     {:9.1} us  cv {:4.1}%  ({:5.1} GB/s weight-stream)",
150                f32_stats.mean_us,
151                f32_stats.cv_percent,
152                f32_bytes / (f32_stats.mean_us * 1e-6) / 1e9,
153            );
154            for (tier, samples) in tiers.iter().zip(&tier_samples) {
155                let tier_stats = stats(samples);
156                let q8_bytes = (n * k) as f64;
157                let verdict = if tier_stats.cv_percent > 5.0 || f32_stats.cv_percent > 5.0 {
158                    "REFUSED (cv>5%)"
159                } else {
160                    ""
161                };
162                println!(
163                    "{label}  q8 {:9} {:9.1} us  cv {:4.1}%  ({:5.1} GB/s weight-stream)  x{:.2} vs f32 {verdict}",
164                    tier.as_str(),
165                    tier_stats.mean_us,
166                    tier_stats.cv_percent,
167                    q8_bytes / (tier_stats.mean_us * 1e-6) / 1e9,
168                    f32_stats.mean_us / tier_stats.mean_us,
169                );
170            }
171        }
172    }
173    println!(
174        "\nNOTE: ratios above compare routes inside this tree (self-comparison = maintenance),\nnever a pinned incumbent. cv%>5 rows are refused, not averaged."
175    );
176}
Source

pub fn dispatch() -> Self

The route the int8 path dispatches by default, honoring the FTTS_INT8_TIER override.

The override exists for interleaved A/B measurement (scalar / autovec / neon-sdot); an unavailable or unrecognized override falls back to the measured default rather than panicking mid-synthesis. Until a per-shape KernelPlan lands, the default is NeonSdot where FEAT_DotProd exists, else Scalar. Measured on M4 Pro (2026-08-08, shape bench, noisy shared host, indicative): plain Scalar autovectorizes to ~50 GB/s and ties SDOT at m=1 — NE-INH-003 reconfirmed — while the hand-shaped Autovec lane loop defeats the vectorizer and loses ~15x; it stays only as an A/B datapoint.

Trait Implementations§

Source§

impl Clone for Int8Tier

Source§

fn clone(&self) -> Int8Tier

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for Int8Tier

Source§

impl Debug for Int8Tier

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for Int8Tier

Source§

impl PartialEq for Int8Tier

Source§

fn eq(&self, other: &Int8Tier) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for Int8Tier

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.