Skip to content

feat: 0.4.0 — what it claimed, checked against what it did - #41

Merged
vyncint merged 2 commits into
mainfrom
release/v0.4.0
Sep 6, 2026
Merged

feat: 0.4.0 — what it claimed, checked against what it did#41
vyncint merged 2 commits into
mainfrom
release/v0.4.0

Conversation

@vyncint

@vyncint vyncint commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #33, closes #34, closes #35, closes #36, closes #37, closes #38, closes #39, closes #40.

The v0.4.0 audit milestone, plus the pin bumps that went with it.

The two correctness fixes

#33eigh answered about a matrix the caller did not pass.

A = [[1, 2], [5, 1]]              (not symmetric)
true eigenvalues       [-2.1622777, 4.1622777]     1 ± √10
eigenvalues of (A+Aᵀ)/2 [-2.5, 4.5]
oxmera 0.3.0 returned   [-2.5, 4.5]      <- the second one, no error

It read both triangles and averaged them, so it was not the lower-triangle
convention either — perturbing either triangle moved the answer. Now a typed
error naming the batch index and the worst pair, above EIGH_SYMMETRY_TOL
(1e-5, relative) — the same bound the CPU↔GPU parity suite uses, so a
covariance matrix assembled in f32 still passes. Verified across the
boundary: symmetric works, noise under the bound works, noise over it is
refused, the check is scale-invariant, and a batched input names the matrix
that is actually wrong.

#34 — nothing tied the shipped PTX to the shipped source.

The crate ships the kernels twice and load_module chooses by driver age, so
a mismatch means two users on one published version run different code. The
only guard compared kernel names. Measured:

$ sed -i '53s|fmaxf(x, 0.0f)|fmaxf(x, 1.0f)|' kernels.cu   # wrong relu
$ cargo test -p oxmera-cuda
test result: ok. 3 passed; 0 failed          <- before
test result: ok. 0 passed; 0 failed; 11 ignored

kernels.ptx.source now carries a fingerprint written by the same just ptx
that writes the PTX. The same mutation now fails, naming the command to fix it.

The one the termlens upgrade found

#40 — driving the dashboard through a pty and signalling it:

q       : exit 0                    alternate_screen after = false
SIGTERM : killed by "Terminated"    alternate_screen after = true   cursor (44,36,false)
SIGINT  : killed by "Interrupt"     alternate_screen after = true

The shell was left inside the alternate screen with the cursor hidden and
nothing written to say so. ratatui::init() already installed a panic hook, so
this was the one remaining unguarded exit. Fixed with signal-hook, which
crossterm already puts in the tree — an import, not a dependency — restoring
and then re-raising with the default disposition, so the process still reports
as killed by that signal.

The rest

#35 CUDA was mandatory on every platform including macOS. Now a default-on cuda feature; --no-default-features drops 7 crates (47 → 40), 88 KB of PTX and a pre-main dlopen. Device::Cuda stays a runtime error.
#36 doctor's capability list was six literals frozen before 0.2.0. Rows now live in the crates that implement them.
#37 crates.io descriptions said "CPU and Apple Metal", two releases after CUDA.
#38 mean over an empty extent returned NaN; now a typed error. sum/max keep their identities — those compose under further reduction and NaN does not.
#39 nn is f32; the error said dtype mismatch … in matmul. Now names the layer and the cast.

Pins

reconverge 0.4.0 → 0.5.0, launchbound 2.0.0 → 2.1.0. The other two of the
four did not move
— reconverge 0.5.0 still records nightly-2026-04-03 and
cuda-oxide a766fc26 — so this is the one-variable case, and the gate was
re-measured rather than assumed:

cargo reconverge check --strict --cc 7.5    0 deny, 0 confirmed, 0 warning
cargo reconverge check --strict --cc 8.6    0 deny, 0 confirmed, 0 warning
launchbound prune --cc 7.5                  12 clean, 0 refused, 0 tool errors
launchbound prune --cc 8.6                  12 clean, 0 refused, 0 tool errors
+20480 tile                                 4 refused, RC004, deny confidence

Identical to the 0.4.0/2.0.0 result. Recorded as docs/research-baseline.md
§(e), which is explicit that only the verdicts are comparable with the
Apple-Silicon sections above it — the timings were not re-measured on this
machine.

termlens 0.9

The pty tests moved to its idioms: bin!, snapshot_after in place of
wait_until + wait_idle + screen() (one settled instant instead of three,
and it waits on the picture holding still rather than on bytes stopping),
wait_stable after a resize, termlens::Result<()> so a failure prints the
grid. quit() now also asserts the terminal came back, so every dashboard test
is a teardown test.

Two guards that caught their own bugs

Worth calling out, because they are the argument for the design bar:

  • The doctor coverage check used report.contains(family) and passed with
    eigh deleted
    — because weights contains eigh. Now whole-word.
  • The first fingerprint wrote FNV-1a's prime as 0x1000_0000_01b3: twelve hex
    digits where 1099511628211 has eleven. The published test vectors now pin it.

Verification

  • 155 tests pass, up from 146. just ci exits 0, including the research workspace.
  • --no-default-features: clippy clean, 10 tests pass, and a CI job asserts
    cudarc/libloading/ctor/oxmera-cuda are absent from the graph.
  • Doctor goldens re-blessed; the diff is four new rows and nothing else.

The audit milestone (#33-#40). Every item was found by exercising main as
a downstream consumer -- building the workspace, driving the CLI through a
pty, probing the public tensor surface op by op -- and every one carries a
reproduction that was run, not inferred from reading the source.

Two are correctness.

eigh silently symmetrized a non-symmetric input (#33): it read both
triangles, averaged them, and returned the eigenpairs of (A + A^T)/2 with
no error. [[1,2],[5,1]] answered [-2.5, 4.5] where the true eigenvalues are
1 +/- sqrt(10), and the returned pair did not satisfy A v = lambda v for
the A that was passed. Now a typed error naming the batch index and the
worst pair, above a relative tolerance chosen to keep passing what the
check exists to permit: a covariance matrix assembled in f32, symmetric in
intent and asymmetric in the last few bits.

Nothing tied kernels.ptx to kernels.cu (#34). The crate ships the kernels
twice and load_module picks between them by driver age, so a mismatch
means two users on one published version run different code. The only
guard compared kernel names: measured, changing relu from fmaxf(x, 0.0f)
to fmaxf(x, 1.0f) passed every GPU-free suite, because the eleven tests
that would catch it are ignored without hardware. A fingerprint written by
the same `just ptx` that writes the PTX now fails that exact mutation.

The termlens 0.9 upgrade found a third. Driving the dashboard through a pty
and signalling it showed alternate_screen() still true and the cursor
hidden after SIGTERM -- ratatui::init() installs a panic hook, so this was
the one remaining unguarded exit (#40). SIGINT/SIGTERM/SIGHUP now restore
and re-raise with the default disposition, so the process still reports as
killed by that signal.

Three were the project describing itself as smaller or older than it is:
doctor's capability list frozen before 0.2.0 (#36), crates.io descriptions
two releases behind (#37), and an undocumented f64 boundary at nn (#39).
One was packaging: CUDA mandatory on every platform including macOS (#35),
now an optional default-on feature that drops seven crates and a pre-main
dlopen when turned off. One was a decision deferred rather than made: four
reductions over an empty extent behaving four ways (#38) -- mean now errors
where it returned NaN, because NaN composes into every gradient and
surfaces an epoch later, while sum's and max's identities compose
correctly.

The design bar was that each fix leaves something behind that fails next
time, and two of those guards caught their own bugs while being written: a
doctor coverage check using contains() passed with eigh deleted, because
"weights" contains "eigh"; and the first fingerprint wrote FNV-1a's prime
with twelve hex digits instead of eleven, which the published test vectors
now pin.

Pins: reconverge 0.4.0 -> 0.5.0, launchbound 2.0.0 -> 2.1.0. The other two
of the four did not move, so this is the one-variable case and the gate was
re-measured rather than assumed: 0 findings at cc 7.5 and 8.6, 12 admitted
and 0 refused at both, over-cap tile still refused with RC004 at deny
confidence -- identical to the old pair. docs/research-baseline.md (e).

termlens 0.8.0 -> 0.9.0, with the pty tests moved to its idioms: bin! for
the spawn, snapshot_after in place of wait_until + wait_idle + screen()
(one settled instant instead of three, waiting on the picture holding still
rather than on bytes stopping), wait_stable after a resize, and
termlens::Result<()> so a failure prints the grid.

155 tests pass, up from 146. `just ci` green, and a second CI configuration
now builds --no-default-features -- which had never been compiled, which is
how a mandatory CUDA backend went unnoticed for two releases.

Closes #33
Closes #34
Closes #35
Closes #36
Closes #37
Closes #38
Closes #39
Closes #40

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Both macOS legs failed while the behaviour under test was identical on
both platforms.

The signal test asserted status.signal() == Some("Terminated"). macOS
spells it "Terminated: 15" and Linux "Terminated", so the exact-string
comparison passed on one and failed the other. contains instead — the
property is that the process still reports as killed by that signal, not
how the platform words it. Note what did NOT fail on macOS: the terminal
was restored and the cursor shown, which is the fix itself.

The no-default-features step asserted ctor was absent. It is not
CUDA-only: oxmera-metal registers at load time the same way, so on macOS
it stays in the graph and should. Now cudarc, libloading and oxmera-cuda
are checked everywhere, and ctor only where the answer is unambiguous.

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@vyncint
vyncint merged commit 60a0802 into main Sep 6, 2026
13 checks passed
@vyncint
vyncint deleted the release/v0.4.0 branch September 6, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment