Winograd streaming forward pass: 5.6-5.9x throughput, portable and safe - #8
Open
ealmloff wants to merge 3 commits into
Open
Winograd streaming forward pass: 5.6-5.9x throughput, portable and safe#8ealmloff wants to merge 3 commits into
ealmloff wants to merge 3 commits into
Conversation
Replace the per-call repeated-tail tensor machinery with a streaming length-aware pipeline built entirely on fearless_simd, and add #![forbid(unsafe_code)]: no FFI, no intrinsics, no platform cfg, and the same engine on every target. - Padded input buffers: each stage writes into a buffer with the conv padding materialized, so every conv block takes the branch-free SIMD fast path with no edge handling in the hot loop. - Precomputed tails (ForwardPlan): every row at or beyond the data-dependent range of a stage equals the same row of a forward pass over an all-padding sequence, computed once per process, including suffix max/sum tables that make the global pool O(1) in the padded tail. Per call, only input-dependent rows are computed. - Endpoint max-pooling: GELU is unimodal with a single minimum at x ~= -0.7517, so max(gelu(x_i)) over a pool group is attained at an endpoint of [min x_i, max x_i] - two GELU evaluations per pooled output instead of one per position. The final stage folds GELU straight into the global max/sum reduction. - Scratch lives in a reused per-thread buffer. cargo bench --bench detect on an M2 Max (same machine, before -> after): - short (68 B): 134.1 us -> 35.9 us (3.7x) - full window (4.6 KB): 2.277 ms -> 753 us (3.0x) detect() reads a fixed <=2 KB window, so per-file cost is constant in file size; a 1 MB input classifies at multiple GB/s. The previous tensor pipeline is kept as logits_reference: a fuzz test checks the fast path against it across 107 unit-sequence lengths (max logit drift < 1e-2; observed ~1e-6 from pooling reassociation), plus a unit test for the pooled endpoint identity. Tree-mode outputs on local repos are unchanged. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
Convert every conv stage to F(4, k) Cook-Toom minimal filtering, cutting multiplies per output tile 1.9-2.8x: weights are transformed once at load, inputs once per stage, and the pointwise stage keeps the group-outer/packed-weight/register-blocked structure that the assembly showed was needed (the previous inner loop spent 695 instructions per 144 FMAs on accumulator spills, bounds checks, and re-streaming the whole kernel per position block; the hot loop is now 22 instructions per 16 FMAs). Transform kernels are generated from exact rational matrices by scripts/gen_winograd.py, with +-a interpolation points factored into shared even/odd halves. fp32 drift versus direct convolution is ~1e-5, two orders below the runtime's Pade GELU approximation error. The old repeated-tail tensor pipeline is gone (~750 lines): the forward plan now builds its input-independent tail rows by running the Winograd stages densely over an all-padding sequence, and the fuzz test checks against a plain scalar oracle written in the tests instead. cargo bench --bench detect on an M2 Max (same machine, this branch before -> after): - short (68 B): 35.9 us -> 23.8 us (1.5x) - full window (4.6 KB): 750.6 us -> 386.3 us (1.9x) Cumulative versus main before this branch: 5.7x and 5.9x. The short case is floor-bound by streaming the ~720 KB of transformed weights through a fixed-cost input; the full window sits within ~15% of the NEON f32 FMA ceiling for the remaining multiply count. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
There is only one forward pass now; drop the qualifier left over from when the reference pipeline existed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rebuilds the forward pass as a streaming, length-aware Winograd pipeline for a ~2x-5.9x single-inference throughput gain (see table), built entirely on
fearless_simd— the crate compiles under#![forbid(unsafe_code)], with no FFI, no intrinsics, no platformcfg, no new dependencies, and the same engine on every target. The old repeated-tail tensor pipeline is deleted outright (~750 lines;layers.rsgoes 951 → 81 lines).Commit 1 — streaming engine:
ForwardPlan, once per process) — every row at or beyond the data-dependent range of a stage equals the same row of a forward pass over an all-padding sequence; suffix max/sum tables make the global pool O(1) in the padded tail. Per call, only input-dependent rows are computed.max(gelu(x_i))over a pool group is attained at an endpoint of[min x_i, max x_i]: two GELU evaluations per pooled output, fused in registers.Commit 2 — Winograd + assembly-driven kernel work:
scripts/gen_winograd.py, with ±a interpolation points factored into shared even/odd halves. fp32 drift vs direct convolution is ~1e-5, two orders below the runtime's Padé GELU approximation error.cargo bench --bench detect, M2 Max, same machine:detect()reads a fixed ≤2 KB window, so per-file cost is constant in file size — a 1 MB input classifies at multiple GB/s; the bench cases are the worst case by construction (noted in BENCHMARKS.md). The short case is floor-bound by streaming the ~720 KB of transformed weights through a fixed-cost input.Test plan
cargo fmt --all -- --check,cargo clippy --all-targets --all-features -- -D warnings,typoscargo test --release: 27 tests + 10 doctests, including a fuzz test comparing the engine against a naive scalar oracle across 18 unit-sequence lengths (tolerance 1e-2 on logits) and a unit test for the pooled endpoint identity over adversarial negative mixes#![forbid(unsafe_code)]enforced at the crate rootGenerated with Devin