From 09dc45cefddfabeb99292f18c8d5dfd27b44d2d8 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:45:26 +0700 Subject: [PATCH 1/2] docs: correct four claims a deep read found false Checked every markdown file against the code before cutting 2.2.0. Links and anchors are sound -- a script walked all 22 files and found 0 broken relative links and 0 broken anchors -- and the measured numbers hold: the five Spearman correlations match model-calibration.toml exactly, the documented `cargo reconverge check --strict --message-format json --cc` invocation matches runner.rs verbatim, and `--cc 86` / `sm_86` / `8.6` all resolve as the README claims. Four things did not hold. **`--allow-unsafe` is on `stage`, not on `tune`.** The README put it in a paragraph immediately after `tune`'s exit codes, so a reader would try `launchbound tune --allow-unsafe` and get "unexpected argument". Verified. Now stated where it lives and what it requires. **The README's CLI block omitted `stage` entirely** -- a real subcommand, listed in docs/ARCHITECTURE.md's crate table but nowhere a user would look. Added, along with `space --list` and `model --results`, both of which exist and neither of which was mentioned. **`just ci` was described with a recipe list two releases stale**, in three places that each name it: AGENTS.md, CONTRIBUTING.md and ci.yml's own header comment. None mentioned `docs`, `versions` or `skill`. A contributor reading any of them would not know what CI runs. **docs/research-baseline.md said "the gate now pins 0.3.0"** in the present tense. It pins 0.6.0. The row records the conditions of the S0 measurements and should not be updated to match the gate -- so it now says which versions followed and points at the two comparison sections, and says explicitly that the row is deliberately frozen. Also: the mermaid diagram used `\n` for line breaks. `
` is the form verified to render on GitHub (termlens's diagram, which was checked visually); `\n` in mermaid *edge* labels is not reliably supported. Switched. Deep test at this commit, all green: `just ci`, `just gate` 3/3 against real reconverge 0.6.0 and cuda-oxide 26754ae5, MSRV 1.88, `just termlens-cli` 4/4, cargo-semver-checks 8/8 crates clean against 2.1.0, corpus prune 93 clean / 8 refused / 0 tool errors, all six kernels lowering to PTX, and the stress hunt 12/12 across both OSes at 1/2/4/8/16 threads. Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- .github/workflows/ci.yml | 5 +++-- AGENTS.md | 3 ++- CONTRIBUTING.md | 3 ++- README.md | 24 +++++++++++++++++------- docs/research-baseline.md | 2 +- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f52b74..ebdac9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ -# The main gate: fmt, clippy -D warnings, tests, cargo-deny, schema -# validation — identical to `just ci` locally, on runners with NO GPU. That a +# The main gate: fmt, clippy -D warnings, tests, rustdoc with warnings denied, +# cargo-deny, schema validation, and the pin/version/skill consistency +# scripts — identical to `just ci` locally, on runners with NO GPU. That a # plain runner stays green is itself the no-GPU path's regression test. name: ci diff --git a/AGENTS.md b/AGENTS.md index 5e151a9..4ae4819 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ is the full contributor document and wins wherever the two disagree. ## Build and test ```sh -just ci # fmt, clippy, test, deny, schemas, pins +just ci # fmt, clippy, test, docs, deny, schemas, pins, + # versions, skill cargo test --workspace # no GPU, no network, no checkout needed just pins # the recorded pin sites agree — no network just gate # the gate tests — needs cargo-reconverge + cuda-oxide diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79fda52..6a789e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,8 @@ runners. ```bash rustup show # installs the pinned nightly from rust-toolchain.toml cargo install just # or: brew install just -just ci # fmt, clippy -D warnings, tests, cargo-deny, schemas +just ci # fmt, clippy -D warnings, tests, rustdoc, cargo-deny, + # schemas, pins, versions, skill ``` `cargo reconverge check` also needs no GPU: it runs over Stable MIR as a diff --git a/README.md b/README.md index ee31087..de43080 100644 --- a/README.md +++ b/README.md @@ -86,11 +86,11 @@ reader arriving from cuda-oxide's docs will assume otherwise; see ```mermaid flowchart LR - A[enumerate] --> B["PRUNE\n(reconverge, MIR — no GPU)"] - B --> C["compile\n(cuda-oxide → PTX)"] + A[enumerate] --> B["PRUNE
reconverge · MIR · no GPU"] + B --> C["compile
cuda-oxide → PTX"] C --> D[benchmark] D --> E[rank] - B -. "disqualified configs\n+ rule ID + source span" .-> F[rejection report] + B -. "disqualified configs
+ rule ID + source span" .-> F[rejection report] D -. "faster but refused" .-> F ``` @@ -140,9 +140,14 @@ Two asymmetries, published rather than buried: ## CLI ``` -launchbound space [--json] # enumerate the space, print its size +launchbound space [--json] [--list] # enumerate the space, print its size launchbound prune --cc 8.6 [--json] # reconverge pass only — NO GPU NEEDED -launchbound model --cc 8.6 # analytical ranking — NOT GATED +launchbound model --cc 8.6 [--results R] # analytical ranking — NOT GATED; + # --results prints Spearman vs measured +launchbound stage --cc 8.6 [--allow-unsafe --reason "…"] + # prune + compile every admitted + # specialization, emit a bench plan + # directory to ship to a GPU box launchbound tune --cc 8.6 --backend cuda|metal|model [--budget 30m] launchbound report [--json] [--rejected] # includes refused-but-faster configs launchbound apply [--no-verify] # emit the cuda-oxide policy specialization @@ -174,8 +179,13 @@ re-verify against — and the output carries a notice saying so. Exit codes: `0` a safe configuration was found; `1` the fastest candidates were refused and the chosen one is slower than a rejected candidate — notable, -not an error; `2` tool error. `--allow-unsafe` exists, requires an explicit -reason string recorded in the report, and is never the default. +not an error; `2` tool error. + +`--allow-unsafe` is **on `stage`, not on `tune`**: measuring a configuration +the gate refused is a deliberate act with its own command, and it requires +`--reason` with a non-empty string, recorded verbatim in the report. A +missing reason is a usage error, not a warning. It is never the default, and +`tune` has no such flag. `prune` needing no GPU is the reason it exists as its own verb: it is the only part of the pipeline a developer on a laptop can run, and it is the part that diff --git a/docs/research-baseline.md b/docs/research-baseline.md index 674e01f..270b2c0 100644 --- a/docs/research-baseline.md +++ b/docs/research-baseline.md @@ -15,7 +15,7 @@ Measured 2026-08-20. | tier-1 guest | Apple `container` 1.2.0, **native arm64** Ubuntu 24.04 (no Docker, no Rosetta — operator requirement), CUDA toolkit 13.2 (sbsa), LLVM 21.1.8, container `cuda-oxide-dev` | | tier-2 box | AWS `g5.xlarge` spot @ **$0.364/hr**, us-east-2c, **NVIDIA A10G** (`sm_86`, cc 8.6), driver 595.71.05, CUDA 13.2 (V13.2.51), LLVM 21.1.8 — chosen over the T4 by the operator; barely above T4 spot ($0.335/hr) | | pinned toolchain | `nightly-2026-04-03` (`rustc 1.96.0-nightly (55e86c996 2026-04-02)`) | -| reconverge | `cargo-reconverge 0.1.11` (built at `~/Projects/reconverge/target/release`) — **the version these measurements were taken with**; the gate now pins 0.3.0, which was verified to admit the identical set (see below) | +| reconverge | `cargo-reconverge 0.1.11` (built at `~/Projects/reconverge/target/release`) — **the version these measurements were taken with**. The gate has since moved to 0.3.0, 0.4.0, 0.5.0 and (in 2.2.0) 0.6.0; each move was verified to admit the identical set — see the two comparison sections below. This row records the conditions of *these* measurements and is deliberately not updated. | | cuda-oxide | checkout `50d07314eb8b7d5ec821ba02b0048a753c20dd4e` — the tree synced to the box (the box AMI's own stale clone reports `e28248c1`, but `./gpu sync` replaces the working tree and excludes `.git`, so the synced tree is what compiled) | | subject kernels | `s0-reduce` (device-only lib crate, dep `cuda-device` only, containing the README's known-flip reduction); cuda-oxide examples `vecadd` (small) and `tiled_gemm` (large) | | evidence logs | `~/Projects/cuda-oxide/.gpu-evidence/20260820T{071248,071807,071959}Z.log` | From f6bd80d40aa8a21f0caee950a7bb7993a16e2e36 Mon Sep 17 00:00:00 2001 From: Vyncint Ng <115854244+vyncint@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:47:38 +0700 Subject: [PATCH 2/2] release: v2.2.0 Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com> --- CHANGELOG.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.lock | 22 +++--- Cargo.toml | 18 ++--- action/action.yml | 2 +- 4 files changed, 200 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8650309..e614aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,67 @@ change measured timings are marked `bench:`. ## [Unreleased] +## [2.2.0] - 2026-09-09 + +The cuda-oxide pin was 133 commits and one toolchain behind, and the watch +that would have said so had its schedule commented out. 2.2.0 catches up, +turns the watch back on, widens the device model past Ampere, and closes the +panic and arithmetic gaps user input could reach — plus the semver and +documentation gates a published 2.x line should have had from the start. + +Ten issues (#50–#59). **Four of them were wrong when filed**, and measuring +before implementing caught each one; the corrections are recorded on the +issues and repeated below, because an audit that miscounts is worth less +than no audit and the shape of the miscount is the useful part. + ### Changed +- **The lockstep pins move to the current upstream**, at every recorded + site: `nightly-2026-04-03` → **`nightly-2026-08-28`**, cuda-oxide + `50d07314` → **`26754ae5`** (upstream `main` on the day), reconverge + `0.5.0` → **`0.6.0`**. The prune leg never needed this — it is analysis + only, which is why the staleness stayed invisible — but the compile leg + shells out to `cargo oxide inspect` from the checkout the pin names, so a + kernel written against a current `cuda-device` could not be built by the + gate at all. + + Verified rather than assumed: the corpus ran under both pin sets and the + outputs were diffed **byte for byte** — 93 clean, 8 refused, the same + eight `REFUSED RC001` lines on `reduce-flip` above one warp, the same + candidate hashes. All six corpus kernels lower to PTX at the new pin + (`.target sm_80`, one `.visible .entry` each). Recorded in + `docs/research-baseline.md` in the shape the 0.1.11 → 0.3.0 comparison + established. (#50) + +- **The README toolchain badge is a checked pin site.** It was not one, and + it had drifted to a nightly nothing installed — the first pin a reader + sees. `check-pins.sh` reports three nightly sites now and fails on badge + drift, which was verified by drifting it on purpose. (#50) + +- **The device model reaches Blackwell.** `DEVICES` held two rows, A10G + (8.6) and T4 (7.5), so `--cc 9.0` on a Hopper part — the architecture a + reader arriving from cuda-oxide is most likely to be holding — was a model + error. Added 8.0 (A100), 8.9 (L4/L40), 9.0 (H100) and 10.0 (B200). + + Every field but `sm_count` is a compute-capability fact from the CUDA C++ + Programming Guide; `sm_count` is a product fact and each row names the + part it came from. The new rows' shared-memory figures were cross-checked + against reconverge 0.6.0's independent `cc.rs` table and agree exactly, + allowing for the 1 KiB the driver reserves per SM on Ampere and later. An + unknown capability now lists the known ones, sorted numerically — `"10.0"` + precedes `"8.6"` as a string, which is the trap. (#52) + +- **`launchbound_bench::Summary` is re-exported from `launchbound-report`.** + It appears in the public fields of `CandidateReport`, `ChosenInfo` and + `RejectedFaster`, so it was reachable and unnameable unless you also + depended on the bench crate. (#55) + +- **`launchbound-space` refuses an impossible block at load.** A block axis + above CUDA's per-axis limit (x, y: 1024; z: 64) is now rejected by + `KernelSpec` with a message naming the value and the limit, so an operator + hears about a typo when the spec loads rather than as a launch failure ten + minutes into a sweep. (#53) + - **The PTY test harness moves to termlens 0.10.1** (from 0.9). The upgrade itself is small — `drag` takes four column-first arguments now instead of two coordinate pairs — but it brings the accessor this suite was missing. @@ -38,6 +97,126 @@ change measured timings are marked `bench:`. summary** via termlens's `report` action, so a red PTY test arrives as a picture rather than a grid in a log. +### Fixed + +- **Six `partial_cmp(..).expect("no NaN")` sorts became `f64::total_cmp`.** + The issue said five; there are seven `partial_cmp` sites and six carried + an `expect`, because the audit grepped the *message string* rather than + the construct and the two with different message text fell out of the + count. Those two were the ones that mattered: `bench/stats.rs` sorts the + **raw measured timings**, and `model/lib.rs` sits under **`spearman`, a + `pub fn`** — so `spearman(&xs, &ys)` with a single NaN in either column + aborted the process from safe code, with no unsafe input path required. + That is a reachable panic, not a latent one. + + `total_cmp` is total, free, and identical to `partial_cmp` on every pair + of non-NaN floats. NaN ordering is documented where it changes an outcome. + In `summarize` it has a consequence worth stating: a NaN fails both Tukey + fence comparisons and is **rejected as an outlier**; poison the quantiles + badly enough and the answer is `None`, the honest summary of a sample that + has none. (#54) + +- **`Config::block_threads()` saturates instead of overflowing.** It was + `.product()` over three `u64`s read straight from `kernel.toml`: a panic + in a debug build, a wrapped value in release — and a wrapped value does + not stop there, it feeds `estimate` and the gate's `threads > WARP_SIZE` + test, so a block of 2^64 + 32 threads reads as a legal 32-thread block. + + The issue proposed `checked_mul` folding to `Result`, which is a break to + a `pub fn` on a crate published at 2.1.0 and could not ship in a minor + bump. It was also unnecessary, and the issue said why without noticing: + `grid_blocks` is the sibling doing the same job with `saturating_mul`. + A proptest over three arbitrary `u64` axes holds the line. (#53) + +- **The summary invariant lives in the type.** `measured_fastest_first` + filtered on `summary.is_some()` and returned bare candidates, so both + callers re-`unwrap()`ed on the strength of a filter performed in another + function. It returns `(&CandidateReport, &Summary)` pairs; the four + unwraps are gone because they no longer compile. Same shape in + `report::build`. Remaining `expect`s in functions that already return + `Result` became `?`. (#55) + +### Added + +- **A semver gate, with the release type forced.** No workflow ran + `cargo-semver-checks`, so removing a `pub fn` shipped without a signal. + The gate forces `--release-type patch` and the reason is measured, not + assumed: remove `Config::kernel` while bumping the manifest to 3.0.0 in + the same PR and the *inferred* run prints `0 checks: 0 pass, 254 skip` and + exits **0**, while forced `patch` prints `1 major and 0 minor checks + failed` and exits **100**. cargo-semver-checks runs only the lints the + declared bump does not already excuse, so a PR that declares its own break + silences the gate. `patch` excuses nothing — and does not forbid + additions, since only breaking changes are reported. + + A deliberate break carries the new `breaking` label, which switches the + job to `major`. Three of the eleven published crates are binary-only; the + eight with a library API are all covered. (#56) + +- **`scripts/check-versions.sh`, in `just ci`.** `[workspace.dependencies]` + pinned the path crates at 2.0.0 while the workspace was 2.1.0 — harmless + for a path build, which is why it survived a whole minor line. Not + harmless for the next major: with the pins stale, bumping to 3.0.0 makes + `cargo metadata` refuse to resolve the workspace at all. **The drift + breaks the release after next.** (#56) + +- **A rustdoc gate, and 237 documented public items.** The issue said "about + 42". It is 237 — I counted items and never counted *fields*, and 144 of + the 237 are public struct fields. In a crate family whose structs are the + JSON wire formats, the fields are the part a consumer actually reads. + + The gate cost nothing to turn on (`cargo doc` was already at zero + warnings) and earned its keep immediately by failing on two broken + intra-doc links in the commit that added it — one naming a function that + does not exist. (#57) + +- **The pin watch runs weekly again, and reports a toolchain move on its own + line.** The schedule had been commented out, so it spoke only when + dispatched. The two kinds of drift are not alike: commit churn is a bump + you can schedule, a channel move means our rustc can no longer build + upstream's kernels and reconverge must be rebuilt. An extractor that reads + nothing now fails the job instead of reporting "Pins current." forever. + Exercised against a stubbed upstream across five scenarios. (#51) + +- **The TUI's freedom from colour is now an invariant.** #58 expected a + colour-only cue to vanish under `NO_COLOR`; measuring found there is no + such cue — every style is `BOLD` or `BOLD | REVERSED` and `Color::` + appears nowhere. So the tests assert the stronger claim: every cell of all + four views has the terminal's default foreground and background, the metal + banner keeps its emphasis under `NO_COLOR=1`, and a `NO_COLOR` frame is + byte-identical to one without it. The first fires the day someone marks a + refusal red. (#58) + + (The issue's other half — "the suite runs at one geometry" — was already + false when filed: 2.1.0's suite runs nine tests at 80×24 and two at 60×30.) + +### Documentation + +- **`#[launch_bounds]` and register budgeting are not validated**, and the + README and LIMITATIONS now say so and link to each other. launchbound + never reads a register count and does not check `#[launch_contract]` + against grid limits. The confusion is earned: `stencil-1d/kernel.toml` + opens by narrating register pressure, `lb_max` is a real tuning dimension, + and the tool is called *launchbound*. The one `.maxntid` relationship the + corpus enforces holds because the kernel author wrote it as a constraint — + launchbound attaches no meaning to `lb_max`. (#59) + +- **The model's device table is narrower than the gate's**, recorded in + LIMITATIONS: reconverge covers 7.0–12.0, so `prune --cc 12.0` can succeed + where `tune --backend model --cc 12.0` refuses. Only 8.6 and 7.5 have ever + had a kernel measured on them here; the other four rows are documented + capacity, not experience. + +- **Four false claims found by a deep read before the release.** + `--allow-unsafe` was documented as if it were on `tune`; it is on `stage`, + and `tune --allow-unsafe` is an "unexpected argument" error. The README's + CLI block omitted `stage` entirely. `just ci` was described with a + two-release-stale recipe list in three separate places. And + research-baseline said "the gate now pins 0.3.0" in the present tense. + +- `docs/RELEASING.md` gains the internal pin bump in step 1 and the + post-publish `baseline-version` move as its own step. + ## [2.1.0] - 2026-09-05 Thirteen findings, all reported against 2.0.0 with a measured reproduction. diff --git a/Cargo.lock b/Cargo.lock index 5e227fc..926c944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" [[package]] name = "launchbound-bench" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "launchbound-space", @@ -1135,7 +1135,7 @@ dependencies = [ [[package]] name = "launchbound-build" -version = "2.1.0" +version = "2.2.0" dependencies = [ "launchbound-space", "serde", @@ -1146,7 +1146,7 @@ dependencies = [ [[package]] name = "launchbound-cli" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "clap", @@ -1164,7 +1164,7 @@ dependencies = [ [[package]] name = "launchbound-metal" -version = "2.1.0" +version = "2.2.0" dependencies = [ "launchbound-bench", "launchbound-space", @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "launchbound-model" -version = "2.1.0" +version = "2.2.0" dependencies = [ "launchbound-space", "serde", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "launchbound-prune" -version = "2.1.0" +version = "2.2.0" dependencies = [ "launchbound-build", "launchbound-space", @@ -1198,7 +1198,7 @@ dependencies = [ [[package]] name = "launchbound-report" -version = "2.1.0" +version = "2.2.0" dependencies = [ "insta", "jsonschema", @@ -1210,7 +1210,7 @@ dependencies = [ [[package]] name = "launchbound-runner" -version = "2.1.0" +version = "2.2.0" dependencies = [ "launchbound-bench", "launchbound-search", @@ -1218,7 +1218,7 @@ dependencies = [ [[package]] name = "launchbound-search" -version = "2.1.0" +version = "2.2.0" dependencies = [ "launchbound-bench", "proptest", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "launchbound-space" -version = "2.1.0" +version = "2.2.0" dependencies = [ "proptest", "serde", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "launchbound-tui" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 3ac490b..b42932e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ exclude = ["corpus"] [workspace.package] -version = "2.1.0" +version = "2.2.0" edition = "2024" # MSRV for crates that do not require the pinned nightly (CONTRIBUTING.md); # set by ratatui 0.30. The analysis and compile paths require @@ -38,14 +38,14 @@ categories = ["development-tools", "development-tools::profiling"] # published crate is compatible with a release it was never built against -- # harmless for a path build, wrong as a record. They sat at 2.0.0 through the # whole 2.1.0 line; `just versions` (and a test) now refuse the drift. -launchbound-space = { path = "crates/launchbound-space", version = "2.1.0" } -launchbound-prune = { path = "crates/launchbound-prune", version = "2.1.0" } -launchbound-build = { path = "crates/launchbound-build", version = "2.1.0" } -launchbound-bench = { path = "crates/launchbound-bench", version = "2.1.0" } -launchbound-report = { path = "crates/launchbound-report", version = "2.1.0" } -launchbound-search = { path = "crates/launchbound-search", version = "2.1.0" } -launchbound-model = { path = "crates/launchbound-model", version = "2.1.0" } -launchbound-metal = { path = "crates/launchbound-metal", version = "2.1.0" } +launchbound-space = { path = "crates/launchbound-space", version = "2.2.0" } +launchbound-prune = { path = "crates/launchbound-prune", version = "2.2.0" } +launchbound-build = { path = "crates/launchbound-build", version = "2.2.0" } +launchbound-bench = { path = "crates/launchbound-bench", version = "2.2.0" } +launchbound-report = { path = "crates/launchbound-report", version = "2.2.0" } +launchbound-search = { path = "crates/launchbound-search", version = "2.2.0" } +launchbound-model = { path = "crates/launchbound-model", version = "2.2.0" } +launchbound-metal = { path = "crates/launchbound-metal", version = "2.2.0" } anyhow = "1" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } diff --git a/action/action.yml b/action/action.yml index 0c57031..e829e9f 100644 --- a/action/action.yml +++ b/action/action.yml @@ -40,7 +40,7 @@ inputs: default: tool-error version: description: >- - Which launchbound release runs the gate, e.g. "2.1.0". The default, + Which launchbound release runs the gate, e.g. "2.2.0". The default, `latest`, installs the newest release on crates.io each run. Pin a number if you want the gate to change only when you say so. required: false