Skip to content

Commit ff03991

Browse files
committed
INT8 quantized runtime, CPU perf work, and a big code/docs cleanup
Quantized runtime: the INT8 PW->DW / DW->PW pairs and the residual/fork tails now run as single streaming kernels (on-the-fly requant, no intermediate boundary tensor), alongside NCHWc weight prepack, native NCHWc pointwise GEMM, VNNI RHS prepack at load, and a SIMD requant epilogue. Output stays bit-identical to the unfused per-op path. fp32 CPU (Siamese tracker vs ONNX Runtime): fixed parallel GEMM being short-circuited by the BLAS path, reworked blocked_gemm parallelism, and added a 12x32 AVX-512 microkernel, pointwise_nx16/16x16 direct kernels, streaming FusedPwDw/FusedDwPw, an AVX-512 c=16 depthwise path, and FusedTransposeMatMul. Cleanup: dropped session-specific notes from comments and deleted 12 dead probe examples; split the eight biggest files (matmul, conv, fused_pw_dw_3x3, backend, tensor ops, the ONNX runner, loader) into directory modules — pure moves, tracker output and latency unchanged. Refreshed the docs to the new layout, re-measured the README tracker numbers, and made cargo doc build clean. Also repointed the doc-count script and two CI microbench thresholds at the new file layout.
1 parent 1e87d77 commit ff03991

128 files changed

Lines changed: 42545 additions & 26367 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ reverted on sight, regardless of benchmark numbers.
7373
together, or stay scalar. Runtime feature detection via
7474
`std::arch::is_*_feature_detected!`. The dispatch pattern is
7575
consistent across the codebase — copy from
76-
`crates/yscv-kernels/src/ops/matmul.rs` if in doubt.
76+
`crates/yscv-kernels/src/ops/matmul/` if in doubt.
7777

7878
3. Minimal code. Smallest correct change. Iterators where the
7979
compiler vectorises them, no `unwrap`, no `#[allow(dead_code)]`,

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Internal refactor: split the largest perf-arc source files (`matmul`,
13+
`conv`, `fused_pw_dw_3x3`, `backend`, tensor `ops`, the ONNX `runner`,
14+
and `loader`) into focused directory submodules. No public API or
15+
behavior change — tracker output stays bitwise-identical and 1T/6T
16+
latency is unchanged within noise.
17+
- Documentation and code comments cleaned of session-specific
18+
optimization-arc notes and refreshed to the current module layout.
19+
1020
## [0.1.9] — 2026-05-02
1121

1222
### Changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The workspace has 18 library crates, 3 application binaries (`apps/bench`, `apps
1414

1515
Key crates and what they do:
1616

17-
- **yscv-tensor** — the foundation. 115 tensor ops in `ops.rs`, f32/f16/bf16 dtype, SIMD (AVX/SSE/NEON).
17+
- **yscv-tensor** — the foundation. 115 tensor ops in `ops/`, f32/f16/bf16 dtype, SIMD (AVX/SSE/NEON).
1818
- **yscv-kernels** — CPU and GPU compute backends. SIMD dispatch (AVX + SSE + NEON with scalar fallback), 61 WGSL + 4 Metal compute shaders, rayon threading.
1919
- **yscv-autograd** — dynamic computation graph with 61 `Op` variants and gradient checkpointing.
2020
- **yscv-optim** — 8 optimizers (SGD/Adam/AdamW/RAdam/RmsProp/Adagrad/Lamb/Lars) all with NEON+AVX+SSE SIMD, Lookahead meta-optimizer, 11 LR schedulers.

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
A complete computer vision and deep learning framework in pure Rust. One `cargo add yscv` gives you image processing (160 ops, faster than OpenCV), neural network training (39 layer types, 8 optimizers), ONNX inference (122 operators, INT4/INT8 quantization), LLM generation (KV-cache, RoPE, GQA), real-time detection + tracking + recognition (67µs per frame), H.264/HEVC/AV1 video decoding (4.5× faster than ffmpeg), hardware decode (VideoToolbox/VAAPI/NVDEC/MediaFoundation), and GPU compute via Vulkan/Metal/DX12 — all in a single statically-linked binary with zero Python or C++ dependencies.
1515

16-
> Project focus. YSCV is built for CPU inference on edge devices — Raspberry Pi, Rockchip / Allwinner SBCs, drone boards, factory PCs, anything ARM Cortex-A or low-power x86. Hot paths are hand-tuned SIMD (NEON / AVX / SSE / scalar) with rayon multi-thread fork-join, profiled and CI-gated against a baseline. On bare-metal ARM SBCs we currently beat ONNX Runtime CPU on a public Siamese tracker; the rest of the perf arc is documented in [`docs/perf-arc-2026-04.md`](docs/perf-arc-2026-04.md). Other backends — wgpu cross-platform GPU, Apple MPSGraph, Rockchip RKNN NPU, Intel/AMD BLAS — exist as opt-in features and keep getting wider, but they're not the headline target. PRs are welcome; see [`CONTRIBUTING.md`](CONTRIBUTING.md).
16+
> Project focus. YSCV is built for CPU inference on edge devices — Raspberry Pi, Rockchip / Allwinner SBCs, drone boards, factory PCs, anything ARM Cortex-A or low-power x86. Hot paths are hand-tuned SIMD (NEON / AVX / SSE / scalar) with rayon multi-thread fork-join, profiled and CI-gated against a baseline. On bare-metal ARM SBCs we currently beat ONNX Runtime CPU on a public Siamese tracker; benchmarks across threads and hardware are in [`docs/performance-benchmarks.md`](docs/performance-benchmarks.md). Other backends — wgpu cross-platform GPU, Apple MPSGraph, Rockchip RKNN NPU, Intel/AMD BLAS — exist as opt-in features and keep getting wider, but they're not the headline target. PRs are welcome; see [`CONTRIBUTING.md`](CONTRIBUTING.md).
1717
>
1818
> Agent-friendly documentation. YSCV is structured so that an AI coding agent can wire it into a downstream project end-to-end without prior context: every crate has a focused `README.md` describing its surface, [`docs/cookbook.md`](docs/cookbook.md) has recipes per task, [`docs/feature-flags.md`](docs/feature-flags.md) is exhaustive on Cargo features and runtime env knobs, [`AGENTS.md`](AGENTS.md) has the workflow + style rules verbatim, and per-op profile labels (`YSCV_RUNNER_PROFILE=path` dumps fused-path JSON) make hot-path issues self-diagnosing. The benefit is downstream: agents can build working code on top of yscv quickly, not the other way around. Responsibility for any PR — including patches drafted by an agent — rests with the human author submitting it.
1919
@@ -134,8 +134,8 @@ Every operation has hand-tuned SIMD on all platforms — NEON on ARM, AVX/SSE on
134134
| YOLO11n MPSGraph | **5.9ms** | all competitors FAIL | **WIN** |
135135
| Siamese tracker 1T (Orange Pi Zero 3, 2026-04-21) | **461.6ms** | onnxruntime 499.3ms | **1.08× faster** |
136136
| Siamese tracker 4T (Orange Pi Zero 3, 2026-04-21) | **150.2ms** | onnxruntime 164.6ms | **1.10× faster** |
137-
| Siamese tracker 1T (Zen 4 7500F, 2026-04-25) | 11.22ms | onnxruntime 8.07ms | 1.39× slower |
138-
| Siamese tracker 6T (Zen 4 7500F, 2026-04-25) | 3.17ms | onnxruntime 1.74ms | 1.82× slower |
137+
| Siamese tracker 1T (Zen 4 7500F, 2026-05-30) | 8.66ms | onnxruntime 1.24.4 8.10ms | 1.07× slower |
138+
| Siamese tracker 6T (Zen 4 7500F, 2026-05-30) | 2.48ms | onnxruntime 1.24.4 1.75ms | 1.42× slower |
139139

140140
Full benchmark results in [docs/performance-benchmarks.md](docs/performance-benchmarks.md).
141141

@@ -156,7 +156,7 @@ The framework is split into 18 crates, each doing one thing well:
156156
| `yscv-track` | DeepSORT, ByteTrack, Kalman filter, Hungarian assignment, Re-ID |
157157
| `yscv-recognize` | Cosine matching, VP-Tree ANN indexing, Recognizer with enroll/match |
158158
| `yscv-eval` | Classification/detection/tracking/regression/image-quality metrics, 8 dataset adapters |
159-
| `yscv-onnx` | 122-op ONNX CPU runtime, INT4/INT8 quantization, LLM generation (KV-cache, RoPE, GQA), graph optimizer, Metal/MPSGraph GPU |
159+
| `yscv-onnx` | 122 op ONNX CPU runtime, INT4/INT8 quantization, LLM generation (KV-cache, RoPE, GQA), graph optimizer, Metal/MPSGraph GPU |
160160
| `yscv-pipeline` | TOML-driven multi-accelerator dispatch (CPU / RKNN / MPSGraph / GPU), RT wiring, recovery, hot-reload |
161161
| `yscv-video-mpp` | Rockchip MPP hardware encoder integration (H.264, H.265) |
162162
| `yscv-cli` | Inference + evaluation CLI: camera diagnostics, dataset eval, pipeline runner |

apps/llm-bench/COMPARISON.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ Fresh desktop recheck on 2026-04-29, median of 3 runs:
5555
| yscv +`--int4-weights 32 --kv-dtype i8` | 1929 | 4568 | 3.50 | 6.40× |
5656
| ORT 1.25, fp32, 12 thr | 69 | 714 | 22.40 ||
5757

58+
Nix ORT recheck on 2026-05-05 used `onnxruntime` 1.24.4 and 8 decode
59+
tokens. The yscv row includes the load-time constant-Transpose fold for
60+
`Transpose(initializer) -> MatMul RHS` and the prepacked-B slice GEMM route
61+
for rank-3 MatMul:
62+
63+
| run | threads | prefill (ms) | decode tok/s |
64+
|---|---:|---:|---:|
65+
| yscv fp32 | 1 | 1223.6 | 8.44 |
66+
| yscv fp32 | 6 | 1176.8 | 8.46 |
67+
| yscv +`--int4-weights 32` | 1 | 1982.3 | 10.68 |
68+
| yscv +`--int4-weights 32` | 6 | 1987.0 | 10.84 |
69+
| ORT 1.24.4 fp32 | 1 | 319.3 | 18.60 |
70+
| ORT 1.24.4 fp32 | 6 | 64.9 | 25.99 |
71+
72+
Before the fold/prepack fix, the same 8-token yscv fp32 harness was ~2.06
73+
tok/s and spent hundreds of milliseconds per forward re-transposing
74+
`model.embed_tokens.weight` for `/lm_head/MatMul`.
75+
5876
The `--int4-weights GS` flag in-process packs every eligible MatMul/Gemm
5977
weight to symmetric INT4 with the supplied group size and re-runs the
6078
exact same harness; runtime dispatch routes through

apps/llm-bench/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ path = "src/bin/quantize_tracker.rs"
3434
name = "bench_tracker"
3535
path = "src/bin/bench_tracker.rs"
3636

37+
[[bin]]
38+
name = "nchwc_coverage"
39+
path = "src/bin/nchwc_coverage.rs"
40+
3741
[dependencies]
3842
yscv-onnx = { version = "0.1", path = "../../crates/yscv-onnx", default-features = false }
3943
yscv-tensor = { version = "0.1", path = "../../crates/yscv-tensor" }

apps/llm-bench/README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ the existing `yscv_onnx::generate` helper.
8989
- `--max-tokens N` — number of new tokens to generate (default 64).
9090
- `--warmup K` — discarded warm-up forward passes before the timed
9191
run (default 2). Catches first-touch allocs / page faults.
92+
- `--threads N` — run yscv through `OnnxRunner::with_threads(N)`;
93+
`0` keeps the default runner policy.
9294
- `--input-name NAME` / `--output-name NAME` — override the input
9395
tensor name (default `input_ids`) / logits output name (default
9496
`logits`) for non-standard exports.
@@ -178,10 +180,12 @@ Single human-readable line on stderr plus a JSON line on stdout:
178180

179181
```
180182
prompt=32t prefill=215.4ms decode=64t / 4823.2ms = 13.27 tok/s total=5039.2ms
181-
{"model":"…/model.onnx","prompt_tokens":32,"decode_tokens":64,"prefill_ms":215.4,"decode_total_ms":4823.2,"decode_tokens_per_sec":13.27,"total_wall_ms":5039.2}
183+
{"model":"…/model.onnx","threads":6,"prompt_tokens":32,"decode_tokens":64,"prefill_ms":215.4,"decode_total_ms":4823.2,"decode_tokens_per_sec":13.27,"total_wall_ms":5039.2}
182184
```
183185

184186
Pipe stdout through `jq` to aggregate across runs.
187+
Set `YSCV_RUNNER_PROFILE=/tmp/llm-profile.json` to dump the same per-node
188+
runner profile schema used by `bench_tracker`.
185189

186190
## Tracker Multi-Input Quantization
187191

@@ -260,14 +264,23 @@ QLinearConv and QuantizeLinear outputs are stored internally as real i8 tensors;
260264
`bench_tracker` reports `quant_i8_stores` and `quant_i8_materializations` so a
261265
run can prove whether it stayed quantized between Conv-like nodes. Entry
262266
`QuantizeLinear` nodes that feed QLinear activations quantize directly into i8
263-
storage through the scalar/AVX2/AVX-512F/NEON dispatch in `yscv-kernels` with packed x86 i8 stores; the
264-
old scalar iterator path is gone from that hot route. For explicit QLinear
265-
pointwise/MatMul shapes, yscv also builds the AVX-512 VNNI 4x16 RHS layout at
266-
model load so repeated tracker runs do not repack the same INT8 weights in the
267-
hot path. The benchmark JSON includes `quant_chain_candidates`,
268-
`quant_chain_executed`, and `quant_chain_fallback`; until fused INT8 chain
269-
actions land, the private tracker QLinear export should show candidates but
270-
zero executed chains.
267+
storage through the scalar/AVX2/AVX-512F/NEON dispatch in `yscv-kernels` with
268+
packed x86 i8 stores; large activation tensors split into per-thread chunks on
269+
multi-threaded runners, and the old scalar iterator path is gone from that hot
270+
route. Residual/fork QLinear chains also parallelize INT8 depthwise
271+
accumulation, NCHW/NHWC layout conversion, and requant/dequant glue. For
272+
explicit QLinear pointwise/MatMul shapes, yscv also builds the AVX-512 VNNI
273+
4x16 RHS layout at model load so repeated tracker runs do not repack the same
274+
INT8 weights in the hot path. The benchmark JSON includes `quant_chain_candidates`,
275+
`quant_chain_executed`, and `quant_chain_fallback`; the candidate count follows
276+
the same greedy non-overlapping ownership as the runtime plan, including
277+
PW->DW / DW->PW kernels, residual/fork suffix actions, and split
278+
`QLinearConv -> DQ` boundaries. On the private tracker QLinear export the
279+
expected Chunk 4 steady state is `quant_chain_candidates=50`,
280+
`quant_chain_executed=50`, `quant_chain_fallback=0`, and
281+
`qlinear_conv_fast=0`, meaning no `QLinearConv` is left to run as an
282+
individual per-node op. The current 3x200 QLinear gate on the private tracker
283+
is 27.63 ms at 1T, 11.67 ms at 4T, and 10.87 ms at 6T.
271284

272285
To reproduce the tracker comparison matrix while tuning quant kernels:
273286

apps/llm-bench/src/bin/bench_tracker.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ fn qlinear_boundary_after(nodes: &[OnnxNode], start: usize, input: &str) -> Opti
143143
fn quant_chain_candidates(model: &OnnxModel) -> usize {
144144
let nodes = &model.nodes;
145145
let mut count = 0usize;
146+
let mut skip = vec![false; nodes.len()];
146147
for i in 0..nodes.len() {
148+
if skip[i] {
149+
continue;
150+
}
147151
let Some(kind) = qlinear_conv_kind(model, &nodes[i]) else {
148152
continue;
149153
};
@@ -157,22 +161,40 @@ fn quant_chain_candidates(model: &OnnxModel) -> usize {
157161
&& ((kind == "pw" && next_kind == "dw") || (kind == "dw" && next_kind == "pw"))
158162
{
159163
count += 1;
164+
for idx in i + 1..=q_idx + 1 {
165+
skip[idx] = true;
166+
}
160167
continue;
161168
}
162-
if let Some((dq_idx, q_idx)) = qlinear_boundary_after(nodes, i + 1, qconv_out) {
163-
let relu_offset = if nodes.get(dq_idx + 1).is_some_and(|n| n.op_type == "Relu") {
164-
1
165-
} else {
166-
0
167-
};
168-
let add_idx = dq_idx + 1 + relu_offset;
169-
if nodes.get(add_idx).is_some_and(|n| n.op_type == "Add")
170-
&& nodes
171-
.get(q_idx)
172-
.is_some_and(|n| n.op_type == "QuantizeLinear")
173-
{
174-
count += 1;
169+
170+
let dq_idx = i + 1;
171+
let relu_idx = i + 2;
172+
let conv_idx = i + 3;
173+
let add_idx = i + 4;
174+
let q_idx = i + 5;
175+
if nodes
176+
.get(dq_idx)
177+
.is_some_and(|n| n.op_type == "DequantizeLinear" && n.inputs.first() == Some(qconv_out))
178+
&& nodes.get(relu_idx).is_some_and(|n| n.op_type == "Relu")
179+
&& nodes.get(conv_idx).is_some_and(|n| n.op_type == "Conv")
180+
&& nodes.get(add_idx).is_some_and(|n| n.op_type == "Add")
181+
&& nodes
182+
.get(q_idx)
183+
.is_some_and(|n| n.op_type == "QuantizeLinear")
184+
{
185+
count += 1;
186+
for idx in dq_idx..=q_idx {
187+
skip[idx] = true;
175188
}
189+
continue;
190+
}
191+
192+
if nodes
193+
.get(dq_idx)
194+
.is_some_and(|n| n.op_type == "DequantizeLinear" && n.inputs.first() == Some(qconv_out))
195+
{
196+
count += 1;
197+
skip[dq_idx] = true;
176198
}
177199
}
178200
count

apps/llm-bench/src/bin/inspect.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,16 @@ fn main() {
4646
println!(" [graph input or external]");
4747
}
4848
}
49+
println!("outputs:");
50+
for out in &node.outputs {
51+
println!(" {out}");
52+
for consumer in m
53+
.nodes
54+
.iter()
55+
.filter(|n| n.inputs.iter().any(|inp| inp == out))
56+
{
57+
println!(" -> {} op={}", consumer.name, consumer.op_type);
58+
}
59+
}
4960
}
5061
}

0 commit comments

Comments
 (0)