blake3: drop the C toolchain requirement; record measured u32/u64 lane codegen - #264
Conversation
Adds `crates/simd-codegen-oracle` (+ script, baseline, CI job) and removes
blake3's C/asm FFI. The oracle answers "does this need intrinsics?" with an
assembly measurement instead of intuition — and its first run falsified the
design assumptions it was built to check.
## Why
TD-T22 measured that under the pinned `-Ctarget-cpu=x86-64-v3`, the
"scalar polyfill" SIMD types already compile to packed AVX2 at the
instruction floor. A PR had hand-written 700 lines of intrinsics against
that nonexistent gap. Nothing in the repo could have caught it: there is no
codegen check, only correctness parity harnesses.
## What the oracle measured
13 probes, three groups. Group A expected to vectorize, Group B expected
NOT to, Group C (u64 rotate) deliberately unclassified.
Group A — all vectorized, as expected. `arx_rounds_u32x16` (10-round ChaCha
double-round): 52 packed, 0 scalar arithmetic on lane data.
Group B — **3 of 5 predictions were WRONG.** LLVM vectorized:
* saturating_abs_i8x32 → vpxor/vpsubsb/vpblendvb, i.e. it synthesized
the VPABSB abs+clamp trick on its own
* widening_u16_to_f32 → vpmovzxwd + vcvtdq2ps
* cross_lane_reverse_u8x64 → vbroadcasti128 + vpshufb + vpermq — a
cross-lane permute, from a scalar index loop
Only serial_dependent_chain (loop-carried dependency) and gather_lookup_u8
stayed scalar. The "cross-lane/widening/saturating obviously needs
intrinsics" intuition is measurably false.
Group C — **the u64 rotate does NOT vectorize.** rot_u64x8 / rot_u64x4:
0 packed, one scalar `rorq %cl` per lane. blake2b_g_u64x8: the leading
`a+b` goes packed (vpaddq), then LLVM extracts every lane to a GPR and
stays scalar through all four rotate stages — including the byte-granular
amounts 32/24/16, which at u32 width fold to vpshufb. AVX2 has
vpsllq/vpsrlq and LLVM applies exactly that shift-or to u32 rotate-by-12/7;
it declines to at 64-bit width.
That matters because BLAKE2b is a 64-bit ARX cipher and argon2 uses
BLAKE2b. The crate has 8 `rotate_left` methods (all u32) and zero
`rotate_right` at any width, so argon2's kernel is unexpressible today.
This is the first intrinsic override meeting the entry criterion: a probe
proving the generic form fails.
Same crate, same week, opposite answers for u32 and u64. That contrast is
the argument for the oracle.
## blake3: no more C
Operator directive — C is a contamination of ndarray, and vendored builds
fail without a C toolchain. blake3's default build ran cc over
c/blake3_{sse2,sse41,avx2,avx512}_x86-64_unix.S: measured 33 .o files plus
libblake3_avx512_assembly.a, with blake3_*_ffi cfgs set. Now pinned to
`default-features = false, features = ["pure"]`, which routes build.rs to
build_sse2_sse41_avx2_rust_intrinsics() ("No C code to compile here").
Verified after `cargo clean -p blake3`: the only cfgs actually SET are
blake3_{sse2,sse41,avx2}_rust; 0 object files, 0 archives, no cc. (The
_ffi names still appear in the build output as rustc-check-cfg
DECLARATIONS — cargo listing valid cfg names — not assignments. A grep
that cannot tell the two apart reports a false positive here.)
Remaining cc build-deps in the lockfile (cmake, openblas-build,
openssl-sys) are reachable only through the optional `blas` feature, not
`default = ["std", "hpc-extras"]`. blake3 was the only C on the default
path — which is why vendoring broke for everyone.
## Honesty rule baked into the tool
The analyzer separates "scalar arithmetic on lane data" from loop control,
so a trip-counter `decl` is never reported as scalar lane work. That exact
overclaim was made and corrected earlier this week; the tool cannot
reproduce it.
## Docs
* .claude/knowledge/simd-one-spec-design.md — design for collapsing 5
backends (31 macro-generated + 57 hand-written types, 13,253 LoC, three
authoring strategies) into one spec, with the oracle as the entry
criterion for intrinsic overrides. Includes the table of my three wrong
predictions.
* .claude/knowledge/crypto-lane-status.md — u32 ARX proven optimal
(ChaCha20/BLAKE3 need no SIMD work); u64 ARX absent and now measured as
genuinely needing intrinsics.
📝 WalkthroughWalkthroughAdds a SIMD codegen oracle with probe kernels, assembly classification, target baselines, CI execution, and documentation describing SIMD lane status and a one-spec multi-backend design. ChangesSIMD codegen verification
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_c2dbd14b-0f5d-482f-bd30-6c0b85426e5e) |
The oracle's first CI run failed with packed=0 on every Group-A probe — arx_rounds_u32x16 came back 0 packed / 146 scalar / 491 memory against 52 packed / 0 scalar locally. Not codegen drift: no AVX2 at all. Cause, verified empirically: $ cargo build -p ndarray --lib -v | grep -o 'target-cpu=[a-z0-9-]*' target-cpu=x86-64-v3 $ RUSTFLAGS="-D warnings" cargo build ... | grep -o 'target-cpu=...' (empty) Cargo's RUSTFLAGS env var REPLACES `[target.'cfg(...)'].rustflags` from .cargo/config.toml; they do not merge. ci.yaml:23 sets `RUSTFLAGS: "-D warnings"` at workflow level, so the v3 baseline is silently dropped for every job that inherits it. Fix: the script now DECLARES its baseline, passing `-C target-cpu=x86-64-v3` after the `--` so it lands on the final rustc invocation and wins over ambient RUSTFLAGS. An oracle that inherits its baseline measures a different machine depending on where it runs — precisely the class of error it exists to catch. Verified with RUSTFLAGS="-D warnings" exported: all 13 probes match. Also recorded in the TD-T22 artifact, because it narrows that finding: "the scalar polyfill compiles to packed AVX2" holds UNDER the v3 baseline, and CI never had it. The baseline does the work, not the source form. NOT changed here: whether CI itself should build at v3 so tests exercise the shipped instruction set. That flips every job's codegen and carries the SIGILL risk TD-SIMD-1 documented — an operator decision, not a drive-by fix in a tooling PR. tier4-avx512-check is unaffected; it uses the more specific CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS, which does not collide.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0aea59a44
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| print("simd-codegen-oracle: ALL PROBES MATCH BASELINE EXPECTATIONS") | ||
| else: | ||
| print(f"simd-codegen-oracle: FAILURES (exit bitmask 0x{exit_mask:x}) -- see FAIL rows above") | ||
| return exit_mask |
There was a problem hiding this comment.
Return a bounded nonzero status for late probe failures
Use a bounded nonzero exit status rather than returning the full bitmask. On Unix, process exit codes retain only the low 8 bits, so a failure confined to probe index 8 or later produces 256, 512, etc. and is observed by the shell as status 0. With the current sorted probe list, missing rot_u64x4/rot_u64x8 symbols or regressions in saturating_abs_i8x32, serial_dependent_chain, or widening_u16_to_f32 therefore leave the new CI job green despite printing FAILURES.
Useful? React with 👍 / 👎.
Two changes. 1. The oracle is no longer a CI job (operator ruling). A standing check that parses assembly to catch a designer skipping a measurement is machinery guarding a lapse the design step should prevent. The question "does this vectorize?" is answered once, during design, and recorded; it is not re-answered on every commit. The crate and script remain as an on-demand instrument. `.github/workflows/ci.yaml` job and its `conclusion.needs` entry removed; YAML re-validated. 2. `codegen_oracle_analyze.py` returned the raw failure bitmask as the process exit status. Unix truncates exit status to the low 8 bits, so a failure confined to probe index >= 8 exits 0x100/0x200/... and the shell observes 0 -- the script printed "FAILURES" and reported success. Demonstrated: a probe-9-only failure exited 512, seen as 0. With probes sorted alphabetically this covered rot_u64x4, rot_u64x8, saturating_abs_i8x32, serial_dependent_chain and widening_u16_to_f32 -- both u64-rotate probes among them, the finding the tool exists to protect. Now exits 1 on any failure; the bitmask stays in the printed line for diagnosis. Found by Codex review. Docs updated to stop claiming CI enforcement and to state why the tool is deliberately on-demand.
The tool is knowledge, not a product artifact. It was adding a crate, two scripts, and a workspace-exclude entry to a published library in order to answer a question that gets asked during design and then recorded. Moved crates/simd-codegen-oracle + scripts/codegen-oracle.sh + scripts/codegen_oracle_analyze.py into .claude/knowledge/simd-codegen-oracle/ (probes.rs, analyze.py, run.sh, baseline-x86_64-v3.toml, README.md). Reverted the Cargo.toml exclude entry. Net effect on the shipped crate: this PR now changes exactly one dependency line (blake3 -> default-features = false, features = ["pure"]) plus its comment. Nothing else outside .claude/. The README carries the runnable recipe, the recorded per-probe results, the declared-baseline rationale, and the exit-status bug, so the instrument is reproducible from where it lives.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/codegen-oracle.sh`:
- Around line 19-47: Update the Cargo invocations in the target-building logic
to execute from $ROOT, such as by wrapping each command with a subshell that
changes to $ROOT before running Cargo. Preserve the existing manifest, target,
and rustc emission arguments while ensuring .cargo/config.toml is resolved from
the repository root.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 47782926-2a41-4027-8f57-71275f9399f9
⛔ Files ignored due to path filters (1)
crates/simd-codegen-oracle/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
.claude/knowledge/crypto-lane-status.md.claude/knowledge/simd-one-spec-design.md.github/workflows/ci.yamlCargo.tomlcrates/simd-codegen-oracle/Cargo.tomlcrates/simd-codegen-oracle/baselines/x86_64-unknown-linux-gnu.tomlcrates/simd-codegen-oracle/src/main.rsscripts/codegen-oracle.shscripts/codegen_oracle_analyze.py
| ROOT="$(cd "$(dirname "$0")/.." && pwd)" | ||
| MANIFEST="$ROOT/crates/simd-codegen-oracle/Cargo.toml" | ||
|
|
||
| TARGET="${1:-}" | ||
| case "$TARGET" in | ||
| "" | -*) | ||
| TARGET="$(rustc -vV | sed -n 's/^host: //p')" | ||
| ;; | ||
| *) | ||
| shift | ||
| ;; | ||
| esac | ||
| # Remaining args (an optional `--` and/or analyzer flags like --verbose) forward as-is. | ||
| if [ "${1:-}" = "--" ]; then | ||
| shift | ||
| fi | ||
|
|
||
| BASELINE="$ROOT/crates/simd-codegen-oracle/baselines/$TARGET.toml" | ||
| if [ ! -f "$BASELINE" ]; then | ||
| echo "==> no baseline for target $TARGET at $BASELINE" >&2 | ||
| exit 90 | ||
| fi | ||
|
|
||
| echo "==> building simd-codegen-oracle (--emit asm) for $TARGET" | ||
| if [ "$TARGET" = "$(rustc -vV | sed -n 's/^host: //p')" ]; then | ||
| cargo rustc --release --manifest-path "$MANIFEST" -- --emit asm -C debuginfo=0 | ||
| else | ||
| cargo rustc --release --manifest-path "$MANIFEST" --target "$TARGET" -- --emit asm -C debuginfo=0 | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate target files =="
git ls-files | rg '(^|/)(codegen-oracle\.sh|Cargo\.toml|config\.toml|baselines/|Cargo\.lock)$|cargo' | sed -n '1,160p'
echo "== script =="
if [ -f scripts/codegen-oracle.sh ]; then
cat -n scripts/codegen-oracle.sh
fi
echo "== oracle cargo files =="
for f in crates/simd-codegen-oracle/Cargo.toml crates/simd-codegen-oracle/.cargo/config.toml crates/simd-codegen-oracle/Cargo.lock; do
if [ -f "$f" ]; then
echo "--- $f ---"
cat -n "$f"
fi
done
echo "== all .cargo/config.toml in repo =="
for f in $(git ls-files '*\.cargo/config.toml'); do
echo "--- $f ---"
cat -n "$f"
done
echo "== run from outside manifests using temp target (read-only probe for files only) =="
mkdir -p /tmp/cargo-config-test
cat >/tmp/cargo-config-test/Cargo.toml <<'EOF'
[package]
name = "test"
version = "0.0.0"
edition = "2021"
[profile.dev]
EOF
mkdir -p /tmp/cargo-config-test/.cargo
cat >/tmp/cargo-config-test/.cargo/config.toml <<'EOF'
[build]
rustc-args = ["--crate-name", "fromconfig"]
EOF
cargo metadata --format-version 1 --manifest-path /tmp/cargo-config-test/Cargo.toml >/tmp/metadata.json
python3 - <<'PY'
import json
data=json.load(open('/tmp/metadata.json'))
pk=data['packages']
print([p['name']+":"+p['manifest_path'] for p in pk])
print("root:", data.get('workspace_root'))
PY
echo "== relevant cargo references in repo =="
rg -n "codegen-oracle|simd-codegen-oracle|target-cpu|x86-64-v3|CARGO|cargo rustc|\.cargo" scripts README.md .github crates 2>/dev/null | sed -n '1,240p'Repository: AdaWorldAPI/ndarray
Length of output: 149
🌐 Web query:
Cargo documentation rustc configuration files discovered based on current directory not manifest path .cargo/config.toml
💡 Result:
Cargo configuration files (such as.cargo/config.toml) are discovered by traversing the file system upward, starting from the current working directory (CWD) where the cargo command is executed, not from the directory containing the Cargo.toml manifest file [1][2][3]. This behavior means that if you run a cargo command from a directory other than the project root (e.g., using --manifest-path), Cargo will not automatically locate or respect configuration files located in the project's root directory or its subdirectories [2][3]. This is a long-standing and known limitation in Cargo [4][5][6]. Key aspects of Cargo configuration discovery include: Hierarchy: Cargo searches for.cargo/config.toml (or.cargo/config) files in the current directory and all parent directories up to the root of the file system [1][7][8]. Precedence: Configuration values are merged, with deeper directories (those closer to the current working directory) taking precedence over ancestor directories [1][9][8]. Global configuration files located in $CARGO_HOME (e.g., $HOME/.cargo/config.toml) have the lowest precedence [1][10][8]. Workspaces: When operating within a workspace, Cargo does not automatically read configuration files from individual member crates; it only considers configurations found relative to the CWD [7][9][8]. Recommended Workaround: To ensure Cargo picks up a specific configuration file, the most reliable method is to change your current working directory to the directory containing the desired.cargo/config.toml file before executing the cargo command [3]. Alternatively, you can use the --config CLI flag to provide specific configuration overrides directly [1][11].
Citations:
- 1: https://doc.rust-lang.org/cargo/reference/config.html
- 2: Specifying the manifest path ignores .cargo/config.toml rust-lang/cargo#10302
- 3: Does cargo read
.cargo/configin the project directory? rust-lang/cargo#7640 - 4: Cargo does not always search for .cargo/config file in project root rust-lang/cargo#2930
- 5: Cargo config search meta issue. rust-lang/cargo#9769
- 6: https://internals.rust-lang.org/t/problems-of-cargo-config-files-and-possible-solutions/12987
- 7: https://doc.rust-lang.org/cargo/reference/config.html?highlight=git%2Crev
- 8: https://doc.rust-lang.org/1.84.0/cargo/reference/config.html
- 9: https://doc.rust-lang.org/1.63.0/cargo/reference/config.html
- 10: https://dev-doc.rust-lang.org/cargo/reference/config.html
- 11: Merged configuration lists have unexpected precedence ordering rust-lang/cargo#12506
🌐 Web query:
Cargo command config file discovered current directory current working directory manifest path
💡 Result:
Cargo discovers configuration files (.cargo/config.toml) by traversing upward from the current working directory [1][2]. It searches the current directory and every parent directory until it reaches the root of the filesystem [1][2]. Additionally, Cargo reads a global configuration file located at $CARGO_HOME/config.toml (which defaults to $HOME/.cargo/config.toml on Unix or %USERPROFILE%.cargo\config.toml on Windows) [1][2]. Crucially, this hierarchical search is based on the current working directory at the time Cargo is invoked, not on the location of the project's Cargo.toml manifest file [3][4]. If you use the --manifest-path option to specify a Cargo.toml file in a different directory, Cargo will still search for configuration files starting from your current working directory, which can lead to unexpected behavior if configuration files exist in those directories [3][4]. For manifest path discovery, Cargo searches upward for a file named Cargo.toml starting from the current working directory [5]. You can use the cargo locate-project command to print the path to the Cargo.toml file that Cargo has resolved [5]. Precedence for configuration values (from lowest to highest) is generally as follows [6][7]: 1. Configuration files in parent directories (hierarchical search) [1][6]. 2. Configuration file in the current working directory [6]. 3. Environment variables (CARGO_*) [6][7]. 4. Command-line overrides (e.g., --config) [6][7]. Note that for configuration files, paths are relative to the parent directory of the directory where the config file is defined [1][2]. For environment variables and --config overrides, paths are relative to the current working directory [1][2].
Citations:
- 1: https://doc.rust-lang.org/cargo/reference/config.html
- 2: https://doc.rust-lang.org/cargo/reference/config.html?highlight=rustflags
- 3: Cargo does not always search for .cargo/config file in project root rust-lang/cargo#2930
- 4: Wrong cargo config resolution when using manifest-path rust-lang/cargo#3808
- 5: https://doc.rust-lang.org/beta/cargo/commands/cargo-locate-project.html
- 6: https://blog.rust-lang.org/inside-rust/2023/08/24/cargo-config-merging/
- 7: Merged configuration lists have unexpected precedence ordering rust-lang/cargo#12506
Run the Cargo command from $ROOT before using --manifest-path.
Cargo resolves .cargo/config.toml from the current working directory, not from the manifest path. Since rustc flags like target-cpu are configured there, running this script from outside the repo root or a subdirectory can bypass the intended x86-64-v3 pin and produce incompatible baseline output.
🔧 Proposed fix
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MANIFEST="$ROOT/crates/simd-codegen-oracle/Cargo.toml"
+cd "$ROOT"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ROOT="$(cd "$(dirname "$0")/.." && pwd)" | |
| MANIFEST="$ROOT/crates/simd-codegen-oracle/Cargo.toml" | |
| TARGET="${1:-}" | |
| case "$TARGET" in | |
| "" | -*) | |
| TARGET="$(rustc -vV | sed -n 's/^host: //p')" | |
| ;; | |
| *) | |
| shift | |
| ;; | |
| esac | |
| # Remaining args (an optional `--` and/or analyzer flags like --verbose) forward as-is. | |
| if [ "${1:-}" = "--" ]; then | |
| shift | |
| fi | |
| BASELINE="$ROOT/crates/simd-codegen-oracle/baselines/$TARGET.toml" | |
| if [ ! -f "$BASELINE" ]; then | |
| echo "==> no baseline for target $TARGET at $BASELINE" >&2 | |
| exit 90 | |
| fi | |
| echo "==> building simd-codegen-oracle (--emit asm) for $TARGET" | |
| if [ "$TARGET" = "$(rustc -vV | sed -n 's/^host: //p')" ]; then | |
| cargo rustc --release --manifest-path "$MANIFEST" -- --emit asm -C debuginfo=0 | |
| else | |
| cargo rustc --release --manifest-path "$MANIFEST" --target "$TARGET" -- --emit asm -C debuginfo=0 | |
| fi | |
| ROOT="$(cd "$(dirname "$0")/.." && pwd)" | |
| MANIFEST="$ROOT/crates/simd-codegen-oracle/Cargo.toml" | |
| cd "$ROOT" | |
| TARGET="${1:-}" | |
| case "$TARGET" in | |
| "" | -*) | |
| TARGET="$(rustc -vV | sed -n 's/^host: //p')" | |
| ;; | |
| *) | |
| shift | |
| ;; | |
| esac | |
| # Remaining args (an optional `--` and/or analyzer flags like --verbose) forward as-is. | |
| if [ "${1:-}" = "--" ]; then | |
| shift | |
| fi | |
| BASELINE="$ROOT/crates/simd-codegen-oracle/baselines/$TARGET.toml" | |
| if [ ! -f "$BASELINE" ]; then | |
| echo "==> no baseline for target $TARGET at $BASELINE" >&2 | |
| exit 90 | |
| fi | |
| echo "==> building simd-codegen-oracle (--emit asm) for $TARGET" | |
| if [ "$TARGET" = "$(rustc -vV | sed -n 's/^host: //p')" ]; then | |
| cargo rustc --release --manifest-path "$MANIFEST" -- --emit asm -C debuginfo=0 | |
| else | |
| cargo rustc --release --manifest-path "$MANIFEST" --target "$TARGET" -- --emit asm -C debuginfo=0 | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/codegen-oracle.sh` around lines 19 - 47, Update the Cargo invocations
in the target-building logic to execute from $ROOT, such as by wrapping each
command with a subshell that changes to $ROOT before running Cargo. Preserve the
existing manifest, target, and rustc emission arguments while ensuring
.cargo/config.toml is resolved from the repository root.
Rung 3a of the ladder, landed as the module + the measurement, WITHOUT the
call-site swap -- because the swap turns out to have a price tag.
## The module
src/hpc/blake3.rs, 771 lines, transcribed from upstream's own
reference_impl/reference_impl.rs (the spec-referenced serial implementation).
No unsafe, no core::arch, no new dependencies. Scoped by the finding that
ndarray's blake3 usage is entirely single-input -- 14 call sites across 8
files use only hash / Hasher::{new,new_keyed,update,finalize,finalize_xof} /
Hash::as_bytes, no hash_many -- so the serial core suffices.
Correctness proven against the official vectors, vendored alongside: 35/35
cases, unkeyed AND keyed, each at both 32-byte length and full extended
length via finalize_xof().fill() -- 140 assertions, input lengths 0..102400.
Plus streaming (one update vs many 37-byte updates over 102400 bytes), empty
input, and incremental fill.
Hash::eq is CONSTANT-TIME. The transcription arrived with a plain `==`, which
leaks match-prefix length through timing when a BLAKE3 output is used as a
MAC; upstream uses the constant_time_eq crate, and since this takes no new
deps the XOR-fold is written inline with a black_box on the accumulator. No
call site compares two Hash values today (seal.rs compares the truncated
MerkleRoot), so it is a guard for future consumers rather than a live fix.
## The measurement, and why the swap is not in this commit
First actual TIME number in this arc -- everything prior was
instruction-class. `.claude/knowledge/blake3-ab-bench/`, release, two runs:
16 B 137 ns vs 100 ns 1.34x
256 B 445 ns vs 350 ns 1.28x
2 KB 4.3 us vs 2.7 us 1.34-1.60x
64 KB 115 us vs 23 us 4.6-5.1x
Two gaps, different causes. The 64 KB gap is the absent hash_many (rung 3b).
The 1.3x small-input gap is NOT -- at 16 B there is one compression and no
parallelism to be had; the crate is faster because it SIMD-accelerates the
single compress itself. Closing that needs a U32x4-shaped compress, a rung
the plan did not have. Call it 3c.
So the earlier framing -- "removes things, needs no benchmark to justify" --
was true about what it removes and silent about what it costs. Dropping the
dependency is a trade, and it is the operator's call, so the call sites still
use the external crate here.
## Four corrections from codex on #268, all valid
- "No CI job compiles the chacha20 ndarray_simd backend" is FALSE.
ci.yaml:141-142 builds vendor/chacha20 for wasm32+simd128, selecting that
backend, under a comment calling it the wasm matryoshka guard. Only the
AVX-512 arm is uncovered. Fixed here AND in the already-merged
chacha20-vendoring-blast-radius.md. Fourth instance of the same
scope-quantifier error: checked the x86 path, generalized to all targets.
- 3a does NOT remove a C build. features = ["pure"] removed all C/ASM in
#264 already. 3a removes the Rust dependency and its intrinsic surface.
Overstating this matters when the counterweight is transcribing a crypto
implementation.
- The ordering arrows from 3a to rungs 4-6 contradicted the same section's
own next sentence and would serialize independent work. Only 3a -> 3b is
real; chacha20's backend already depends on ndarray, dalek has no cycle,
and the u64 lane lives inside ndarray.
- [[patch.unused]] is NOT cargo's cycle diagnostic. Cargo reports the cycle
by naming the chain (cargo update -p blake3 did); an unused patch only
means the patch was not selected, usually a version mismatch or a stale
lockfile. Conflating them teaches misdiagnosis and undermines the
cargo-tree check the entry prescribes.
4 blake3 tests pass; clippy clean; fmt clean.
…nsic Records what shipped rather than what was planned: the native VPROLVQ/VPRORVQ override exists on avx512 only; avx2/scalar/nightly carry per-lane loops that are measured not to vectorize, and the explicit vpsllq/vpsrlq shift-or for avx2 is NOT written. Stating that as a table avoids the reading that all six backends got the override. Also reconciles two lines the rung-6 change made stale: the 'rungs 4-6 are optional' summary, and the 'only 3a removes a C build' clause, which #264's features = ["pure"] had already made false and which rung 3a's own section already corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfbkUCBbtZhy3yjSfCDHp
…iming doc Addresses the CodeRabbit pass on #268. Taking the findings that hold and saying which do not, rather than applying all nine. FIXED — correctness: - **`derive_key` had zero vector coverage.** The parser read `input_len`, `hash`, and `keyed_hash` and skipped past the `derive_key` field already present in the file. It is the one public mode built as TWO passes (DERIVE_KEY_CONTEXT over the context string, whose output keys a DERIVE_KEY_MATERIAL pass over the material), so a swapped or collapsed transcription would have been invisible while hash/keyed_hash stayed green. Now asserted for all 35 cases at both the 32-byte and extended XOF lengths. It passed on the first run — the transcription was right; it just wasn't proven. Control-tested: swapping the two flags fails ONLY the new assertion. - **`Hash::eq`'s documentation described the opposite of the code.** The doc-comment said `read_volatile` (the code uses `core::hint::black_box`) and the not-transcribed list flatly asserted "`PartialEq` here is a normal short-circuiting byte-array compare" — stale since the constant-time fold landed. On a MAC-comparison path a backwards timing claim is worse than no claim: an auditor would have believed this file leaks match-prefix length when it does not. Also now states that `black_box` is a best-effort barrier, not a guarantee; the data-independent loop is the actual property. FIXED — hazards: - `run.sh` copied the bench to a fixed `examples/blake3_ab.rs` and had the EXIT trap `rm` it unconditionally: a pre-existing file there would be clobbered and then deleted. Now refuses to overwrite, and only removes the directory it created. - `shiftor_rot_u64x8` is `pub` and took arbitrary `u32` into `a >> n` — a debug panic and a release wrapping-shift for `n >= 64`. Normalized at the PUBLIC wrapper and deliberately NOT in the measured inner body, which would have changed the codegen the oracle exists to observe. - Rotate test extended past 64 (65/127/128/191) against std's `u64::rotate_*`, pinning the mod-64 contract rather than only the width case. - Test-local context const renamed `VECTORS_CONTEXT`; as `DERIVE_KEY_CONTEXT` it shadowed the module-scope u32 flag of that name. - Redundant `.into()` in `derive_key` (`clippy::useless_conversion`). - The `pub mod blake3` incident history was an OUTER doc comment, so rustdoc would publish it as module documentation. Now a plain `//` comment. Three more stale doc claims, all the same class already corrected elsewhere in this PR — an assertion outliving the change that falsified it: - EPIPHANIES said "an error there means no cycle and the work is unblocked" one paragraph after explaining the error is ambiguous. Read literally it marks blocked work unblocked on the strength of a typo. Now a four-step procedure with the error explicitly inconclusive. - crypto-lane-status prescribed an explicit shift-or for AVX2/NEON/wasm, contradicting the measurement directly above it (LLVM folds the two forms byte-identically) and misdescribing what shipped. - blake3-in-tree-measured still credited the swap with removing the C build, already removed by `features = ["pure"]` in #264. NOT taken, with reasons: - Doctest examples on every public API. Worth doing, but it is a separate change across a surface this PR is not otherwise touching, and the vectors already smoke-test the public path harder than an example would. - Caching the compressed block in `OutputReader`. A real inefficiency for small repeated `fill` calls, but unmeasured, and the module is explicitly not on a hot path pending the swap ruling. - Deduplicating the rotate body across backends. Every other lane type in this crate is duplicated per backend by design; collapsing one method would make it the exception, not the rule. - Secret-scanner allowlist for the vectors file. Correct that the hex is published test data, not credentials, but no scanner config lives in this repo to add it to. Full lib suite green: 2200 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfbkUCBbtZhy3yjSfCDHp
One dependency line changes in the shipped crate. Everything else is documentation under
.claude/knowledge/.The change
blake3's default build compiles hand-written assembly throughccand links it — 33 object files and one static archive. Any vendored or offline build ofndarraytherefore requires a C toolchain, and fails without one.pureroutesbuild.rstobuild_sse2_sse41_avx2_rust_intrinsics(), which compiles no C.ccinvokedVerified after
cargo clean -p blake3: the only cfgs set areblake3_{sse2,sse41,avx2}_rust.Cost: the hand-tuned x86-64 assembly is replaced by Rust intrinsics; the AVX-512 path and the aarch64 NEON C intrinsics are dropped.
cmake,openblas-buildandopenssl-sysalso build C, but are reachable only through the optionalblasfeature, never throughdefault = ["std", "hpc-extras"]. blake3 was the only C on the default path.Supporting measurement
Under the
-Ctarget-cpu=x86-64-v3baseline, the macro-generated lane types do not compile to scalar code. 13 probes:vpaddd/vpxor/vpshufbreduce_sumvfmadd213pssaturating_absvpxor/vpsubsb/vpblendvbvpmovzxwd/vcvtdq2psvpshufb/vpermqrorq, one per lanemovzblchain, no arithmeticThe u64 rotate is the outlier. LLVM lowers u32
rotate_left(12)/(7)viavpslld/vpsrld/vporand folds byte-granular amounts tovpshufb. At 64-bit width it does neither — including for byte-granular 32/24/16 — despitevpsllq/vpsrlqbeing available.This is load-bearing for the crypto surface: BLAKE2b is a 64-bit ARX cipher and argon2 uses BLAKE2b. The crate has 8
rotate_leftmethods, all u32, and norotate_rightat any width, so argon2's kernel cannot currently be expressed. AVX-512 provides_mm512_rorv_epi64as a single instruction.The converse also holds: hand-writing intrinsics for the u32 lanes would lose to the optimizer, which already emits 8
vpadddfor 64 u32 lanes — the AVX2 minimum.Documentation added
.claude/knowledge/simd-codegen-oracle/— the measurement instrument (probes, analyzer, baseline, README with the runnable recipe and recorded results). Filed as knowledge rather than as a crate or CI job: the question "does this vectorize?" is answered once during design and recorded, not re-answered on every commit.simd-one-spec-design.md— design for consolidating five backend files (31 macro-generated types, 57 hand-written, 13,253 LoC) behind one declaration, with a measured entry criterion for intrinsic overrides.crypto-lane-status.md— per-lane status: u32 ARX and f32 FMA need no work; u64 ARX is absent and needs intrinsics.td-t22-asm-investigation.md— extended with the CI baseline finding below.Incidental finding: CI does not apply the v3 baseline
.github/workflows/ci.yamlsetsRUSTFLAGS: "-D warnings"at workflow level, and cargo'sRUSTFLAGSenv var replaces[target.'cfg(…)'].rustflagsfrom.cargo/config.tomlrather than merging:Every job inheriting that
env:—tests/{stable,beta,1.95.0},clippy,native-backend,hpc-stream-parallel— compiles at baseline x86-64 rather than the AVX2 tier. The comment in.cargo/config.tomlstating "this is what GitHub CI runs against" is inaccurate.tier4-avx512-checkis unaffected; it uses the more specificCARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS.Deliberately not changed here. Switching CI to v3 alters the instruction set every test exercises and carries the SIGILL risk documented in TD-SIMD-1. Separate decision, separate PR.
Out of scope
The u64 rotate implementation, the one-spec migration, and BLAKE3's remaining Rust intrinsics — a second SIMD surface that
puredoes not remove. Closing that means implementing BLAKE3's compression onndarray::simd::U32x16, since it is a ChaCha-derived u32 ARX kernel and that lane is already at the instruction floor.