Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .cargo/config-v3.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# x86-64-v3 (AVX2) — the PORTABLE DISTRIBUTION BASELINE, pinned explicitly.
#
# Usage:
# env -u RUSTFLAGS cargo --config .cargo/config-v3.toml <cmd>
# CARGO_ARGS='--config .cargo/config-v3.toml' bash scripts/masking-parity.sh native
#
# v3 is portable across all x86_64 silicon shipping since ~2013 (Haswell+) and
# is what a general-distribution build should target. Until 2026-09-16 it was
# `.cargo/config.toml`'s default and therefore UNNAMED at every call site; the
# default is now `target-cpu=native` (see that file's superseded-in-place note
# for why), so the tier a row depends on is named BY that row. This file is
# where "portable" is spelled.
#
# Overlay semantics, measured 2026-09-16: cargo JOINS `target.<cfg>.rustflags`
# across config files and the LAST `-Ctarget-cpu` wins, so this file needs the
# target-cpu ONLY — `.cargo/config.toml`'s two crypto-backend cfgs
# (`curve25519_dalek_backend="serial"`, `poly1305_force_soft`) come through the
# join and stay in force. Verified with `cargo build -p encryption -v`: both
# `-Ctarget-cpu` values present, v3 last, both cfgs present.
#
# `env -u RUSTFLAGS` is load-bearing: a RUSTFLAGS env REPLACES every
# cargo-config rustflags entry, so with one set this file does not apply AND
# neither do the crypto cfgs.
#
# No `-Dwarnings` here, deliberately — unlike `config-v4.toml`. A config that
# promotes warnings to errors turns a disable-run (which typically orphans a
# binding) into "did not compile", which reads identically to "the guard was
# not load-bearing" when piped through a grep. The portable arm is the one most
# likely to be used for a disable-run, so it stays warnings-permissive; the
# repo's `-D warnings` gate is the explicit clippy invocation.
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-Ctarget-cpu=x86-64-v3"]
72 changes: 57 additions & 15 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,64 @@
[build]
# Default cargo config — x86-64-v3 (AVX2) baseline. Portable across all
# x86_64 silicon shipping since ~2013 (Haswell+). This is what GitHub CI
# runs against and what `cargo build` produces for general distribution.
# Default cargo config — `target-cpu=native`: the default MEASURES THE MACHINE
# IT RUNS ON.
#
# Why v3 and not "no target-cpu":
# ### ⊘ SUPERSEDED 2026-09-16: this file used to pin x86-64-v3 as the default
#
# The v3 pin was correct about one thing and wrong about the consequence. It IS
# the portable distribution baseline — that part stands and is now spelled
# explicitly in `.cargo/config-v3.toml`. What it got wrong is that a default
# naming a tier the host is NOT means every AVX-512 measurement needs an
# incantation, and a forgotten incantation does not fail — it silently grades
# the wrong tier. Measured the day of the flip: `scripts/codegen-witness.sh
# avx512` run WITHOUT `CARGO_ARGS='--config .cargo/config-v4.toml'` built v3 and
# reported three FAILs ("has no vpternlog on an AVX-512 build") on probe symbols
# the change under test never touched. The assertion was right, the build was
# the wrong one, and nothing in the output said so.
#
# `native` cannot mis-grade that way: rustc resolves the host CPUID, so the
# default arm is always the arm this machine can actually run, and it can never
# SIGILL by construction. The cost is stated plainly rather than hidden: the
# default no longer NAMES a tier, so **a measurement is labelled by the arm the
# program itself reports** (`simd-masking-parity` and every probe print
# `avx512f=true|false`), never by "it was the default". A tier a row depends on
# is pinned by that row, explicitly:
#
# portable / distribution baseline --config .cargo/config-v3.toml (v3, AVX2)
# ^ REQUIRED for anything you ship. A plain
# `cargo build --release` is now tuned to the BUILD HOST and is not portable
# — `.cargo/config-native.toml` has always said so about this exact flag
# ("do NOT distribute artifacts built with this config"), and that warning now
# applies to the default. Runtime `simd_caps()` dispatch does NOT rescue it:
# detection picks among code paths, it cannot un-emit host-only instructions
# the baseline codegen already placed everywhere. (Raised by codex on #313;
# README's build table and both Dockerfiles were corrected in the same PR.)
# Nothing is shipped from a default build today — this crate is a `[lib]` with
# no bin targets, and the published crate does not carry this file to
# consumers, who build under their own config. The caveat is for THIS
# workspace's own release artifacts and images.
# AVX-512 --config .cargo/config-v4.toml (v4)
# Sapphire Rapids (VNNI/BF16/AMX) --config .cargo/config-avx512.toml
#
# This does NOT reach `.github/workflows/ci.yaml`. That workflow sets a global
# `RUSTFLAGS: "-D warnings"`, and a RUSTFLAGS env REPLACES every cargo-config
# rustflags entry — so none of the flags in this file have ever applied there,
# including the two crypto cfgs below. Measured two-sided on the same unit the
# same day: no RUSTFLAGS → 65× `-Ctarget-cpu`, 65× `poly1305_force_soft`;
# `RUSTFLAGS="-D warnings"` → zero of each.
#
# Why NOT "no target-cpu at all":
# `src/simd_avx2.rs` composes `F32x16` as two `__m256` halves (AVX
# intrinsics), and the `simd_avx2_*` op funcs use `__m256i` (AVX2).
# Without a global v3 baseline, rustc compiles to x86-64 generic (SSE2)
# and those intrinsics emit instructions the CPU never executes →
# SIGILL at run time, exactly the PR #170 CI failure mode.
#
# AVX-512 builds: use `--config .cargo/config-avx512.toml` (or
# `CARGO_BUILD_RUSTFLAGS='-Ctarget-cpu=x86-64-v4'`). The simd.rs dispatch
# arms key off `target_feature = "avx512f"`; under v4 they pick the
# `simd_avx512` backend (native `__m512` / `__m512d` / `__m512i`).
# With NO baseline at all, rustc compiles to x86-64 generic (SSE2) and
# those intrinsics emit instructions the CPU never executes → SIGILL at
# run time, exactly the PR #170 CI failure mode. `native` clears that floor
# on any host that has the features, and on a host that does not, the
# backend `simd.rs` selects is the one that host can run.
#
# Build-machine-tuned binaries: use `--config .cargo/config-native.toml`
# (`target-cpu = "native"`); rustc resolves the host CPUID at compile.
# `.cargo/config-native.toml` is kept and is now a no-op overlay on x86_64
# (same flag as the default). It stays because it names the intent at a call
# site and because a caller may pass it on a host whose own default config
# differs.
#
# Runtime LazyLock dispatch (one release binary, heterogeneous deployment
# silicon) is a fifth opt-in mode — see § 7.1 of
Expand Down Expand Up @@ -80,7 +122,7 @@
# Verify: cargo build -p encryption -v 2>&1 | grep poly1305_force_soft
[target.'cfg(target_arch = "x86_64")']
rustflags = [
"-Ctarget-cpu=x86-64-v3",
"-Ctarget-cpu=native",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve portability for the documented release build

When cargo build --release runs on an AVX-512/VNNI host, this setting permits LLVM to emit those host-only instructions throughout the binary, so deploying that artifact to another supported x86-64-v3 machine can terminate with SIGILL before runtime SIMD dispatch helps. This is the repository's documented “Automatic SIMD detection” build (README.md:200-208), while .cargo/config-native.toml:10-11 explicitly warns that the identical setting is not portable; either keep v3 as the default or update every distribution-facing build command to pass --config .cargo/config-v3.toml.

AGENTS.md reference: AGENTS.md:L9-L12

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it caught a real gap in my reasoning: I thought about measurement (which arm a probe grades) and never about distribution (what a plain release build yields). Fixed in 038e96d0 by taking your second option — naming the tier at every distribution-facing command rather than reverting the default.

  • README.md — the portable build is now cargo --config .cargo/config-v3.toml build --release. The host-tuned build stays, labelled "portable nowhere". Your point about runtime dispatch is the load-bearing one and is now in the text: simd_caps() picks among code paths, it cannot un-emit host-only instructions the baseline codegen already placed.
  • .cargo/config.toml — carries the caveat, with its real scope stated rather than overstated: nothing ships from a default build today, because this is a [lib] with no bin targets and the published crate does not carry this file to consumers, who build under their own config. The caveat is for this workspace's own artifacts and images.
  • README.md's AVX-512 line had the same shape (RUSTFLAGS="-C target-cpu=x86-64-v4") and is now --config .cargo/config-v4.toml, for the reason below.

Chasing this turned up two more instances of the defect the PR's third commit is about. Both Dockerfiles set ENV RUSTFLAGS="-C target-cpu=x86-64-vN". A RUSTFLAGS env replaces every cargo-config rustflags entry rather than joining it, so that form sets the tier and silently drops .cargo/config.toml's two crypto-backend cfgs — meaning both images have been shipping curve25519-dalek's and poly1305's raw-intrinsic AVX2 backends, which is exactly what those cfgs exist to keep out. Converted both to --config, which joins. Measured two-sided on the exact commands:

form tier poly1305_force_soft
ENV RUSTFLAGS="-C target-cpu=x86-64-v3" v3 absent
cargo --config .cargo/config-v3.toml v3 present

So your P1 was worth more than the one file it pointed at.


Generated by Claude Code

"--cfg",
"curve25519_dalek_backend=\"serial\"",
"--cfg",
Expand Down
170 changes: 170 additions & 0 deletions .claude/blackboard.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,173 @@
## 2026-09-16 (14) — the default `target-cpu` now MEASURES THE HOST; a config that a caller can silently REPLACE was never a guarantee

Three findings, one root cause, PR #313 (branch `claude/c64-6502-falsifier-shztkk`).
The root cause is one sentence: **a build flag you did not have to ask for is a
flag you cannot tell you got.**

### 1. The flip: `.cargo/config.toml` → `target-cpu=native`

It used to pin `x86-64-v3`. That is the correct PORTABLE baseline and still is —
it moved into `.cargo/config-v3.toml`, where a row that needs it SAYS so. What
was wrong was making it the *unnamed default*, because then every AVX-512
measurement needs an incantation, and a forgotten incantation does not fail.

**The incident that prompted it, same session.** Gating the `pack<const L>` fold
I ran `scripts/codegen-witness.sh avx512` WITHOUT
`CARGO_ARGS='--config .cargo/config-v4.toml'`. It built v3 and printed:

```
probe_ternlog_u64x8: 0 vpternlog
FAIL: probe_ternlog_u64x8 has no vpternlog on an AVX-512 build
```

Three FAILs, on probe symbols the change under test never touched. The assertion
was right, the build was the wrong one, and **nothing in the output said which**.
That is the workspace's own recorded trap — *a timing without its target-cpu is
an anecdote* — with a second door: an ASSERTION without its target-cpu is a
false alarm, and a false alarm on symbols you did not touch is the shape most
likely to be believed.

After the flip the identical bare command PASSES with 6 `vpternlog`.

### 2. The pin is load-bearing in BOTH directions — measured two-sided

A flip that only made v4 easier would have moved the landmine, not removed it.
On this AVX-512 host:

| command | result |
|---|---|
| `codegen-witness.sh avx2` bare | **FAIL** `has no packed logic` — grading v4 assembly against "no vpternlog may appear" |
| `codegen-witness.sh avx2` + `config-v3` | **PASS** |

So the matrix's portable row now PINS v3 instead of inheriting it. Unpinned it
would grade whichever tier the runner SKU happens to be — and some Azure runner
generations carry AVX-512, so it would be **nondeterministic across reruns**,
which is worse than wrong.

**The rule this generalizes to: a tier is READ, never inferred.** Every probe
and the parity program print `avx512f=true|false` precisely because the default
no longer names a tier. Cite that line, not "it was the default".

### 3. The bigger find, and it was NOT the one I went looking for

`.github/workflows/ci.yaml` sets a workflow-global `RUSTFLAGS: "-D warnings"`.
A RUSTFLAGS env **REPLACES** every cargo-config `rustflags` entry rather than
joining it. So from the moment that variable was introduced, **nothing in
`.cargo/config.toml` had ever applied to any job in that workflow** — not the
target-cpu, and not the two cfgs that compile out curve25519-dalek's AVX2
backend (57 raw `_mm*` under 52 `unsafe`) and poly1305's (424 under 30).

Those are the second and third unaudited SIMD surfaces beside `ndarray::simd`,
in the crypto path. The config file argues at length for keeping them out of the
binary — the matryoshka rule. **In CI they were in.**

Measured two-sided, same tree, same unit (`cargo build -p encryption -v`):

| RUSTFLAGS | `-Ctarget-cpu` | `poly1305_force_soft` |
|---|---:|---:|
| unset | 65× | 65× |
| `-D warnings` | **0** | **0** |
| `-D warnings` + the two cfgs | — | present, clean |

Fixed by putting the two **arch-neutral** cfgs into that global RUSTFLAGS.
`-Ctarget-cpu` stays out for the reason it was removed (i686 is 32-bit, s390x is
not x86); both cfgs are read by their crates on every arch.

**⊘ Correction to how this file has been reasoning.** Several earlier entries
cite `.cargo/config.toml:83` as evidence of what a build *was*. That is sound
only when no RUSTFLAGS env is set. **Before citing any config flag as in force,
check whether the caller sets RUSTFLAGS.** Entries (13) and earlier are not
wrong — they used `env -u RUSTFLAGS` — but the habit of citing the file rather
than the arm's own report is what let this sit unnoticed.

### 4. Two real defects the flip surfaced on DAY ONE

Both in code the v3 default never compiled, and therefore never linted:

- `src/simd_int_ops.rs` — `needless_return` in the runtime-VNNI block. The lint
is **config-dependent**: the trailing scalar fallback is cfg'd out when
`avx512vnni`/`avxvnni` is a compile feature, so the second `return` is
trailing there and load-bearing on v3. **`allow`, not `expect`** — `expect`
would fail the v3 build for the lint NOT firing, turning one arm's cleanup
into the other arm's error. Worth keeping: a cfg-dependent lint is the one
case where `expect` is the wrong tool.
- `examples/ternlogq_tail_descent_probe.rs` — `print_literal`. Gated
`avx512f + avx512vl`, so #311's own clippy run never compiled it. My #311
commit message claimed "clippy v3 `--examples --tests` clean"; that was TRUE
and did not cover this file. **A green lint over code that was cfg'd out is
not evidence about that code.**

### 5. The open question — ANSWERED the same day, and the answer is bigger

A new `host-native` matrix row runs the unpinned parity program and is
**`continue-on-error` on purpose**: a row whose answer is "whatever this
runner is" cannot gate a merge on pool scheduling.

**Measured on its first run, and it beat the question.** Within ONE workflow
run (35148155422, head `c1bd7015`), two jobs — both `runs-on: ubuntu-latest`,
both under the `target-cpu=native` default — reported different tiers:

| job | reports |
|---|---|
| `realization/nightly x x86_64` | `avx512f=TRUE` |
| `realization/host-native x x86_64` | `avx512f=FALSE` |

**GitHub's `ubuntu-latest` pool is HETEROGENEOUS: the tier is decided per
JOB, not per run and not per repo.** So `native` in CI is a coin flip, and
an ISA assertion on an unpinned row would pass or fail on scheduling. That
is the empirical vindication of pinning the portable row — a green unpinned
run would have proven only that the day's scheduling was lucky.

**⊘ Correction to this session's own reasoning, recorded because the error is
instructive.** When the nightly row failed I inferred "the GitHub runner has
AVX-512" from the failure's mechanism alone (the errors sat in
`#[cfg(all(test, target_feature = "avx512f"))]` modules, so that predicate
had to be true). The inference was locally valid and the generalization was
wrong: it was true of THAT job, and false of another job in the same run. **A
mechanism that proves a fact about one runner proves nothing about "the
runner".** The `host-native` row is what caught it, which is the whole reason
a row that only reports is worth having.

### 5b. What the nightly CI failure actually was — a REAL bug, not collateral

`realization/nightly x x86_64` went red on the first push. Root cause, and it
is the flip earning its keep rather than the flip breaking something:

`cargo +nightly test --features nightly-simd` **fails to compile on ANY host
where `avx512f` is a compile-time feature**, and has for as long as both
existed. The call sites live in `#[cfg(all(test, target_feature = "avx512f"))]`
modules of `src/simd_avx512.rs`; under the old v3 default that predicate was
false, so the two features never co-compiled anywhere — not in CI, not
locally. Any developer on an AVX-512 machine hits it today.

The gap was a stated-contract violation: both polyfill files' own doc comments
say *"API mirrors `simd_avx512::<Type>` so consumer code is backend-agnostic"*,
and four types were short — `I8x64`/`I8x32` (zero, add, sub, cmp_gt) and
`I16x32`/`I16x16` (those plus min, max).

**Fixed the surface, did not pin the row.** Pinning the nightly row to v3
would have hidden a defect that bites outside CI and stopped that row ever
witnessing the combination again — "disable the thing that found the bug".

Semantics were READ off the native bodies, not guessed: `add`/`sub` are
`_mm512_add/sub_epi{8,16}`, i.e. WRAPPING, so the polyfill uses `+`/`-` and
NOT the `saturating_*` methods sitting next to them, which are a different
operation and the obvious way to get this subtly wrong. `cmp_gt` delegates to
each type's existing `cmpgt_mask` so the two spellings cannot drift.

Evidence is a RUN, not a lint: `cargo +nightly test --lib --features
nightly-simd` -> **2534 passed**, and the ones that matter are the AVX-512
backend's OWN test vectors now executing against the `core::simd` polyfill and
agreeing with the native expectations. That is cross-backend parity this repo
did not previously have.

### What did NOT change

v3 is still the portable distribution baseline. The SIGILL floor argument in the
config file still holds (`simd_avx2.rs`'s `__m256`/`__m256i` bodies need AVX2
present) — `native` clears it on any host that has the features, and on a host
that does not, `simd.rs` selects a backend that host can run.

## 2026-09-16 (13) — the `VPTERNLOGQ` tail is a DESCENT, not a pad (5–8×); a 64×2 re-apply on a full-width mask is NOT (0.5–0.7×); the GEMM block-stop tail is INERT (0.99–1.02×)

Three probes, one question in three places (operator: *"instead of padding the
Expand Down
27 changes: 26 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,32 @@ env:
# `.cargo/config.toml`: per-function `#[target_feature]` + runtime
# `LazyLock<Tier>` detection means one binary, all ISAs. Jobs that
# specifically need a higher target-cpu can opt in via per-job env.
RUSTFLAGS: "-D warnings"
#
# The two `--cfg` flags are NOT a preference — they are the matryoshka
# guarantee, and they are here because SETTING THIS VARIABLE AT ALL SILENTLY
# DISABLED THEM (found 2026-09-16). A RUSTFLAGS env REPLACES every
# cargo-config `rustflags` entry rather than joining it, so from the moment
# this workflow gained a global RUSTFLAGS, NOTHING in `.cargo/config.toml`
# applied to any job here — including the two cfgs that compile out
# curve25519-dalek's AVX2 backend (57 raw `_mm*` intrinsics under 52
# `unsafe`) and poly1305's (424 under 30). Those are second and third
# unaudited SIMD surfaces beside `ndarray::simd`, in the crypto path, and
# `.cargo/config.toml` argues at length for keeping them out. In CI they
# were in.
#
# Measured two-sided on one unit, same tree, same day:
# no RUSTFLAGS env -> 65x -Ctarget-cpu, 65x poly1305_force_soft
# RUSTFLAGS="-D warnings" -> ZERO of each
# RUSTFLAGS="-D warnings <the two cfgs>" -> both cfgs present, build clean
#
# Only the ARCH-NEUTRAL half is restored here. `-Ctarget-cpu` stays out for
# the reason above (i686 / s390x); both cfgs are read by their crates on
# every arch, so they are safe across the whole cross matrix.
#
# Rule for anyone editing this line: a flag added to a RUSTFLAGS env does not
# ADD to the cargo config, it REPLACES it. Re-read `.cargo/config.toml`
# before changing this value and carry forward anything still needed.
RUSTFLAGS: '-D warnings --cfg curve25519_dalek_backend="serial" --cfg poly1305_force_soft'
MSRV: 1.98.1
BLAS_MSRV: 1.98.1

Expand Down
Loading
Loading