All notable changes to this project are documented in this file.
The format is based on Keep a Changelog,
and this project adheres to
Semantic Versioning. Entries that
change measured timings are marked bench:.
-
The CUDA path has been run, on silicon, at the current pin set (#83) — the first time anything automated has. An A10G (cc 8.6, driver 595.71.05, CUDA 13.2) with
cargo-reconverge 0.7.0,cargo-oxide 0.2.1and cuda-oxideb0f961df: the gate admitted 3 and refused 8, all three admitted candidates compiled, and all three measured.block_x=32 tile=128came in at 0.033792 ms — the 0.0338 ms the README quotes, from a run a month earlier on a different instance of the same part under a different analyzer and a different cuda-oxide. The provenance and the full numbers are indocs/research-baseline.md.One observation recorded rather than smoothed over: at these durations (~30–75 µs) two of the three 95% intervals are zero-width and every median is a multiple of 1024 ns, which is the CUDA event timer's granularity showing through. It does not affect this ranking, and it is noted where the noise floor is discussed.
The production-readiness release. It moves the lockstep set onto a reconverge that can see a class the old one could not, puts tests under the part of this tool that decides what the number is, and closes the supply chain and release gaps that let a published version go unannounced for twelve days.
-
The lockstep pin set moves: cuda-oxide
b0f961df, reconverge0.7.0, nightly unchanged (#70). Its own commit across every sitescripts/check-pins.shlists, because the policy is "every site or not at all" and 2.0.0 did four of six.reconverge 0.7.0 is the release that matters here rather than a routine bump. Before it, a cluster- or grid-wide barrier under a guard that is only block-uniform produced no findings at all — so the gate this product is built on could not see that class, and 2.2.1 shipped with a reconverge that could not.
Verified at the new set rather than assumed:
just gatepasses all three gate tests — the most important in this repository — andjust pruneover the whole corpus returns the same verdicts, includingreduce-flip's 3 clean / 8 refused, which is the known-flip split the README quotes. The new analyzer changes what it can see and does not change what this corpus is told.
-
A dispatchable workflow that re-runs the CUDA path. Nothing automated had ever run it. There is no GPU job anywhere else here,
crates/launchbound-bench/src/cuda.rshas no test that executes, and the headline result in the README — the A10G sweep where six refused configurations measured up to 3.00x faster — was produced by hand on 2026-08-20 and has not been reproduced since. The lockstep pin set has moved twice in between, anddocs/LIMITATIONS.mdalready records a known way for the compile step to break. Nothing would have said so.gpu.ymlsplits the work the waystageandlaunchbound-runnerwere designed to be split: pruning and compiling happen onubuntu-latest, and the box-side binary ships beside the plan, so the expensive machine needs a driver and nothing else — no toolchain, no cuda-oxide checkout, no analyzer. It isworkflow_dispatchonly, because it costs money, and withmeasure: falseit stages a plan for a box you drive by hand.It gates on the claims that port — the schema, no candidate in the results that is not in the plan, no admitted candidate that failed to run, something measured — and records the numbers with their device, driver and plan capability rather than asserting them. Timings are exactly what does not transfer between parts.
It does not provision the machine: this repository holds no cloud credentials, and adding one is a decision with a blast radius rather than a workflow detail.
-
Issue forms, a pull-request template and
CODEOWNERS..github/heldscripts/andworkflows/and nothing else.The forms matter more here than they would for a library, because of what a report has to contain before anyone can act on it: a result depends on the GPU, the driver, the CUDA version, the compute capability, the cuda-oxide commit, the nightly and which of the three backends produced it, and
docs/LIMITATIONS.mdhas a section on exactly that. "The tuner picked a slow configuration" is unactionable; the same report with its provenance block is a bug.Three of them — a wrong convergence verdict in either direction, a measurement that disagrees with what you observe, and the known hole where a gate-clean candidate still fails the real compile. Blank issues are off, and a finding that is wrong about the kernel rather than the launch shape is pointed at reconverge, which this project ships as a component and does not reimplement.
-
Tests for the part of this tool that decides what the number is. 94 tests for 7,571 lines, and the distribution was the finding: the best-tested crates were the ones that present a result or refuse a candidate, and the ones that produce it were the thin ones. 123 now.
-
The exit-code contract, which the README states and nothing checked.
launchbound-clihad three tests, all aboutparse_budget, and no integration test at all. Exit 1 — "the fastest candidates were refused and the chosen one is slower" — is the product's whole argument in one integer, and it is the one a refactor can quietly turn into 0, because the text on stdout is identical either way.The recorded Metal sweep has no refused candidate (
gate: "none"), so asserting against it as-is would have been exactly the kind of test that cannot fail. The test marks the two fastest candidates refused — two, because their 95% intervals overlap each other andrejected_fasterrequires the refused interval to sit wholly below the chosen one's, which is the same "indistinguishable, never ranked" rule the ranking uses — and then requires exit 1. Replacing the branch withExitCode::SUCCESSfails it; nothing else in the suite notices.Also checked:
--allow-unsafewithout--reason, and with a blank one;--reasonwithout--allow-unsafe; thattunehas no--allow-unsafeat all; that a malformed--ccis refused before anything is spawned; and that--jsonand the text report never disagree about the status. -
Checkpoint and resume.
run_planneeds a device, but the two halves that decide whether a resume is safe do not. A checkpoint misread as "no checkpoint" silently re-measures candidates that cost GPU time, so each untrustworthy shape — not JSON,results.v2, noschemaat all — is now required to fail with the path named. And the write-then-rename leaves no.json.tmpbehind. -
The Metal path.
launchbound-metalhad one test, about rewriting a constant in a string. Six now: the no-gate notice is asserted as text rather than trusted as a constant, the off-macOS stub must refuse rather than return an empty sweep, a CUDA-only dimension with no MSL twin is skipped rather than invented, and an unterminated constant is named rather than silently truncating the source. -
The model's ranking, pinned against the real corpus kernel rather than a synthetic space, as a relation rather than as numbers: the same space ranks the same way twice, and sorting by cost is a total order with nothing infinite in it. Every other test there checked a piece of the cost function; none checked that the pieces compose into the same order. Plus: an unknown capability must list the ones that exist.
-
-
CI executes a Metal dispatch. The macOS leg compiled the
cfg(target_os = "macos")code and then ran a string-rewriting test;run_metal— the function behind "On Apple Silicon it measures on Metal" — had no caller outside the CLI, and the only evidence the path worked was one committed sweep on one machine. A smoke step now tunes one corpus kernel under a 60s budget and reads the run back. It asserts that a dispatch happened and produced a readableresults.v1, not that the numbers mean anything: timings from a shared runner are not measurements.
-
The GitHub Release is created by the release workflow, and the semver baseline is checked. Two version literals nobody compiles, and both had drifted.
release.ymlhad nogh releasestep at all — it tested, published eleven crates, moved the floating tag and notified the testing repository, and never created the release. Every one had been cut by hand, so skipping one took only a distraction: 2.2.1 went to crates.io on 2026-09-10 and the repository still showed 2.2.0 as Latest, for the release whose whole content was making every relative link in the README stop 404ing. Thegithub-releasejob builds the notes from this version'sCHANGELOG.mdsection rather than from--generate-notes, which would be a downgrade from a changelog written for a reader.ci.yml's--baseline-versionmoves to 2.2.1. The comment beside it already said what the rule was — the literal moves in the release PR after the publish — and it had not been followed, so the gate was measuring every pull request against 2.2.0 and anything that changed incompatibly in 2.2.1 was outside what it could see.scripts/check-versions.shnow holds the literal equal to the newest version on the index, so the next time this drifts it failsjust ciinstead of waiting to be noticed. Unreachable index: skipped with a notice, because a gate that fails offline is a gate people learn to skip. -
docs/LIMITATIONS.mdcontradicted itself about the T4, and claimed a measurement that never happened. Line 147 said "Only 8.6 (A10G) and 7.5 (T4) have ever had a kernel measured on them here"; line 167, in the section on results not porting, said "nothing has been measured on a T4". The evidence agrees with the second:docs/research-baseline.mdrecords the tier-2 box as ag5.xlargewith an A10G, chosen over the T4 by the operator, andmodel-calibration.tomlnames one device.This is not a footnote.
README.mdpoints at this file twice — "read that before trusting a result" — and the paragraph exists specifically to tell a reader which rows of the model's device table are experience and which are documented capacity. Someone deciding whether to act on a--cc 7.5ranking was reading a sentence that said it had been validated on silicon. Five of the six rows are capacity, not four. -
launchbound-runneraccepted a malformed--budget-secsin silence, on the machine that costs money. It parsed every value with.parse().ok(), which discards the error, so:typed meant --budget-secs 30mno budget — the spelling tune --budgettakes, prints in--help, and anyone types from memory--budget-secs nanno budget — elapsed >= NaNis false for every elapsed--budget-secs -5exhausted before the first candidate --seed abcseed 0, and then recorded as random:0, so the provenance disagreed with what was typed--budget 30ma missing file named --budgetThe laptop-side binary has had this right for a while:
parse_budgetinlaunchbound-cli, whose doc comment is a written account of these exact failures — theusize - 1panic on an empty value,"NaN".parse::<f64>()succeeding,1e400arriving as infinity. All of that rigour was on the binary that cannot spend a GPU-hour, and none of it on the one that can.It is one function now, in
launchbound-benchbeside thebudget_secsit fills, taking the flag name so a message sends the reader to the right binary's--help. The runner refuses an unparseable--seed, a flag with no value, an unknown flag and a third positional path, and its crate has tests — it was the only one in the workspace with none. Restoring.parse().ok()fails three of the seven, which is how they were checked.
-
termlens 0.11 for the TUI's PTY suite, the vendored skill, and the
termlens-clipins the report action uses inci.ymlandstress.yml(check-skill-version.shholds all three equal to the dependency). 0.11 is termlens's stability candidate: from it no promised item changes incompatibly before its 1.0.Its one breaking change lands here as a simplification.
Screen::unsupported()returns a view instead of a slice ofArc<str>, andunsupported_overflow()folds into it — so the pinned list and "the record is not truncated" are now one assertion incrates/launchbound-tui/tests/emulation.rs, because the view compares equal to a slice only when the retained shapes match and nothing overflowed the bound.The pin's known-defect caveat is gone with it: termlens#320, which named blink and strikethrough as unsupported although its attribute shadow implements them, was fixed upstream in 0.10.2.
-
Every relative link in the README was a 404 on all eleven crates.io pages. All eleven crates set
readme = "../../README.md", so this file ships as each one's readme — and crates.io rewrites a relative link against the crate's directory in the repository rather than the repository root.docs/SAFETY.mdwas served as…/blob/HEAD/crates/launchbound-space/docs/SAFETY.md, which does not exist, and the same eight links were dead on every crate:docs/SAFETY.md,docs/ARCHITECTURE.md,docs/LIMITATIONS.md(twice, once with an anchor), the Action directory,rust-toolchain.tomland both licences.Nothing here could see it. The same file renders correctly on GitHub, where it really is at the root, so the only broken surface was the one every reader arriving from crates.io meets first — and
docs/SAFETY.md, the document the README calls "the product", was among the dead.The links are absolute now.
scripts/check-readme-links.sh(injust ci) refuses a relative one, checks every target still exists in the checkout, and checksblobversustree— GitHub redirects ablobURL for a directory rather than failing, so a checker that only reads status codes calls it fine.Reaches future releases only: crates.io renders each version's readme as published, so the 2.2.0 pages stay broken.
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.
-
The lockstep pins move to the current upstream, at every recorded site:
nightly-2026-04-03→nightly-2026-08-28, cuda-oxide50d07314→26754ae5(upstreammainon the day), reconverge0.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 tocargo oxide inspectfrom the checkout the pin names, so a kernel written against a currentcuda-devicecould 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 RC001lines onreduce-flipabove one warp, the same candidate hashes. All six corpus kernels lower to PTX at the new pin (.target sm_80, one.visible .entryeach). Recorded indocs/research-baseline.mdin 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.shreports three nightly sites now and fails on badge drift, which was verified by drifting it on purpose. (#50) -
The device model reaches Blackwell.
DEVICESheld two rows, A10G (8.6) and T4 (7.5), so--cc 9.0on 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_countis a compute-capability fact from the CUDA C++ Programming Guide;sm_countis 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 independentcc.rstable 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::Summaryis re-exported fromlaunchbound-report. It appears in the public fields ofCandidateReport,ChosenInfoandRejectedFaster, so it was reachable and unnameable unless you also depended on the bench crate. (#55) -
launchbound-spacerefuses an impossible block at load. A block axis above CUDA's per-axis limit (x, y: 1024; z: 64) is now rejected byKernelSpecwith 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 —
dragtakes four column-first arguments now instead of two coordinate pairs — but it brings the accessor this suite was missing. -
tests/emulation.rspins what the emulator drops. Every screen assertion in this crate reads a grid a VT emulator produced from the binary's bytes: five golden files, two border scans and a styled banner. A sequence that emulator does not implement makes the grid quietly wrong and every one of those assertions a claim about a plausible-looking fiction. 0.10 made it checkable, and the answer is oneSGR 59— underline colour, which changes no cell. Pinned exactly, so anything joining it has to be read before the suite is trusted again. -
tests/cli.rsdrivestermlens-cliagainst this crate's own saved screens: rendered, diffed with its 0/1/2 exit codes, andinspectpointed at the real binary. The tool is resolved at the version the lockfile names, so it and the library are one release. -
The vendored agent skill (
.claude/skills/termlens/SKILL.md) is refreshed to 0.10.1, andcheck-skill-version.shnow fails when that copy and the dependency disagree on major.minor. It had drifted two releases behind with nothing to notice. -
CI writes
TERMLENS_ARTIFACT_DIRand renders failures into the job summary via termlens'sreportaction, so a red PTY test arrives as a picture rather than a grid in a log.
-
Six
partial_cmp(..).expect("no NaN")sorts becamef64::total_cmp. The issue said five; there are sevenpartial_cmpsites and six carried anexpect, 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.rssorts the raw measured timings, andmodel/lib.rssits underspearman, apub fn— sospearman(&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_cmpis total, free, and identical topartial_cmpon every pair of non-NaN floats. NaN ordering is documented where it changes an outcome. Insummarizeit 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 isNone, the honest summary of a sample that has none. (#54) -
Config::block_threads()saturates instead of overflowing. It was.product()over threeu64s read straight fromkernel.toml: a panic in a debug build, a wrapped value in release — and a wrapped value does not stop there, it feedsestimateand the gate'sthreads > WARP_SIZEtest, so a block of 2^64 + 32 threads reads as a legal 32-thread block.The issue proposed
checked_mulfolding toResult, which is a break to apub fnon 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_blocksis the sibling doing the same job withsaturating_mul. A proptest over three arbitraryu64axes holds the line. (#53) -
The summary invariant lives in the type.
measured_fastest_firstfiltered onsummary.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 inreport::build. Remainingexpects in functions that already returnResultbecame?. (#55)
-
A semver gate, with the release type forced. No workflow ran
cargo-semver-checks, so removing apub fnshipped without a signal. The gate forces--release-type patchand the reason is measured, not assumed: removeConfig::kernelwhile bumping the manifest to 3.0.0 in the same PR and the inferred run prints0 checks: 0 pass, 254 skipand exits 0, while forcedpatchprints1 major and 0 minor checks failedand 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.patchexcuses nothing — and does not forbid additions, since only breaking changes are reported.A deliberate break carries the new
breakinglabel, which switches the job tomajor. Three of the eleven published crates are binary-only; the eight with a library API are all covered. (#56) -
scripts/check-versions.sh, injust 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 makescargo metadatarefuse 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 docwas 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 isBOLDorBOLD | REVERSEDandColor::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 underNO_COLOR=1, and aNO_COLORframe 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.)
-
#[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.tomlopens by narrating register pressure,lb_maxis a real tuning dimension, and the tool is called launchbound. The one.maxntidrelationship the corpus enforces holds because the kernel author wrote it as a constraint — launchbound attaches no meaning tolb_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.0can succeed wheretune --backend model --cc 12.0refuses. 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-unsafewas documented as if it were ontune; it is onstage, andtune --allow-unsafeis an "unexpected argument" error. The README's CLI block omittedstageentirely.just ciwas 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.mdgains the internal pin bump in step 1 and the post-publishbaseline-versionmove as its own step.
Thirteen findings, all reported against 2.0.0 with a measured reproduction. Most of them reduce to one shape: something was consumed on a contract other than the one it is written to, and the mismatch was reported as a fault of whatever it was pointed at.
-
The lockstep pins move to reconverge 0.5.0, at every recorded site. 2.0.0 moved the gate to 0.4.0 and updated four of the six the policy names, so
rust-toolchain.toml,CONTRIBUTING.mdandpins.ymlstill recorded 0.1.11 — andpins.ymlmeasures upstream drift against its ownRECONVERGE_PIN, so its weekly signal reported movement away from a version nothing installs. That is why #17 sat open describing a pin two releases old.just pinsnow asserts the sites agree with each other, with no network, before anything asks upstream. Reported in #46 and #17. -
apply --verifyis a real switch. It was aboolwithdefault_value_t = true, which clap gives aSetTrueaction — so--verifyset what was already set,--no-verifywas an unknown argument, and the help read as an opt-in for something mandatory. There is a--no-verifynow, and the help says verification is on. This is what makesapplyusable on a machine that has the run directory but not the analyzer and the pinned toolchain. Reported in #36. -
prune's verdict line says what it checked. The gate answers convergence and static shared-memory capacity at a--cc; it has no view of instruction availability, so a crate using ansm_80+intrinsic underneeds_cc = "7.5"prunes to12 cleanat--cc 7.5and fails only when something finally lowers it for that part.needs_ccis the author's claim and is taken on trust — defensible, and nowhere stated, so "clean" read as "this kernel is fine at cc 7.5". docs/LIMITATIONS.md carries the long form, including why the two stronger fixes were not built. Reported in #32.
-
A kernel crate with a bin target no longer hard-stops the gate. reconverge prints one
findings.v1document per analyzed target — its documented contract — and this reader handed the whole of stdout to a singlefrom_str, so asrc/main.rsbeside a kernel library, the ordinary shape of a GPU crate, made every candidate a tool error attrailing characters at line 2 column 1. In the Actionfail-ondefaults totool-error, so CI went red for a crate with nothing wrong with it, pointing at the analyzer's tracker. Stdout is read as JSONL and the findings are unioned: a deny finding in any target refuses, and the bin target's document is harmless to merge. The fail-safe always held — it held against a format the analyzer documents. Reported in #42. -
The scratch copy is the whole crate. It took only the entries of
src/that are files, somod util;withsrc/util/mod.rs— how Rust code is organised past one file — produced a scratch crate that could not compile, and the gate reportederror: could not compileagainst a crate whose owncargo checkis clean. The message told its author to fix build errors they do not have, or to reinstall their toolchain, and never said that what it compiled was not their crate. It copies recursively now, carriesbuild.rsor whateverpackage.buildnames, skipstarget/, and a tool error names the scratch directory. Reported in #43. -
rustc's diagnostics survive the tool-error filter. #19 replaced a tail-six heuristic with a filter on marked lines, and picked the secondary marker: rustc's primary diagnostics begin
error[E0583]:, a code before the colon. So what survived was cargo's summary and reconverge's generic hint — "see the errors above", with the one that was above removed. Both forms are accepted now, here and in the compile executor, which still had the tail heuristic #19 removed next door. Reported in #44. -
--budgetcannot be given a value that fails to bound anything.split_at(text.len() - 1)on a trimmed-empty argument is0usize - 1, so--budget ""panicked at exit 101. Worse:NaNand1e400parsed, and the guard iselapsed >= budget— false for every value against NaN, never true against infinity — so a value that looked accepted produced an unbounded measured sweep on real silicon, which is the one failure a budget exists to prevent. Non-finite and negative values are rejected, the message names the flag and the accepted forms, andmin/hrare accepted alongsidem/hbecause that is what people type.--budget 0stays valid and means what it always did. Reported in #33. -
applydecides about verification before it prints anything. It emitted theparams.rsand then verified, so "refusing to emit" arrived after the emission and a reader who had piped stdout to a file had the file. On a Metal run it could never succeed at all: that path has no convergence gate, deliberately, so the run recordsgate_cc: "metal", and handing that sentinel to reconverge produced the correct answer to the wrong question ("metalis not a compute capability") dressed as a regression ("no longer passes the gate"). Nothing regressed; the gate never ran and cannot. It refuses by name now, before stdout, and points at--no-verifyorprune --cc <target>.Verdicthas aDisplay, so no user-facing message is a Rust struct literal. Reported in #34. -
A
results.jsonthe report cannot read is an error, not "unmeasured". A truncated file, an empty one,null,[], aresults.v2from a newer runner and a directory namedresults.jsonall rendered as "nothing measured yet": exit 0, nothing on stderr, and a JSON report that validated against the schema. The run directory is the hand-off between two machines and those two conditions call for opposite actions — wait, or go and look. OnlyNotFoundisOk(None)now; everything else names the path and the cause, the wayverdicts.v1already did fifteen lines away in the same function.model --resultsnames the cause too, andtune's end-of-run report inherits all of it. Reported in #45. -
Nothing is cut mid-value or mid-word at the panel border. #24 fixed this on the chosen line and left it in the two views below: the ranking lost every closing bracket at eighty columns, so each interval read as a number with no upper bound, and the rejections view — the one the README calls the point of the tool — lost the clause that says what to do, stopping at
splits a 64-threaand never reachingsafe only at one warp (<= 32 threads). An interval is a field and is dropped whole through the same helper the chosen line uses; a reason is a sentence and wraps. A scan over every golden now fails on a value or a word ending at the border without an ellipsis — confirmed red against the shipped pre-fix frames, and it would have caught #24. Reported in #35. -
A missing
cargo oxideis diagnosed by name. cargo's own help relayedcargo search cargo-oxide, and cargo-oxide is not on crates.io: cuda-oxide is a pinned git checkout. The one actionable-looking line sent the reader to a package that does not exist, on the first wall ofstageandtune --backend cuda. The message now names the pin, gives the three commands CI uses, and says thatpruneneeds none of it. The pin is a constantjust pinschecks, so a bump moves the message with it. In the same path,exit Some(101)is an exit code again and the compile failure reports the compiler's errors rather than its summary. Reported in #37. -
No flag on
tuneis accepted and silently ignored.--budget,--orderand--seedare inert with--backend model, and--seedis inert with--order exhaustiveon any backend. Each says so once, the way--outhas since #22 — somebody who passes--budget 30mreasonably believes something is bounded. Reported in #38.
A major, and both reasons are in the "breaking" list this project keeps in docs/RELEASING.md: a changed CLI flag, and a change to what the gate admits.
-
The safety gate pins
reconverge0.4.0, up from 0.3.0, and 0.4.0 reads a shared-memory length written as a named const. 0.3.0 could only read a literal:SharedArray<f32, TILE>arrived as an unevaluated const, the analyzer'seval_target_usizerefused it, and the static was dropped from the RC004 budget with no finding and no diagnostic.This is not an abstract gap — it is the shape every tunable kernel has.
corpus/matmul-tileddeclaresSharedArray<f32, { TM * TK }>, and launchbound rewritesTM/TKper candidate, so every configuration this tool tries took the path RC004 could not see. A space with an over-cap tile pruned as all-clean.Verified against the corpus: the six kernels' verdicts are unchanged (their tiles are well under the cap), and raising
matmul-tiledtoTM = TK = 128now produceserror[RC004]: kernel matmul declares 73728 bytes, where 0.3.0 said nothing. Any kernel whose shared memory is sized by a const may now be refused where it previously passed — which is the gate working, and is why this is a major. -
--ccis required bytune, as it already was bypruneandmodel. It defaulted to8.6, which meant the one command whose answer you act on quietly picked a device, while the two inspection commands made you choose.prune --cc's own help says a verdict at one capability does not transfer to another; RC004 is a capacity check, and 8.6 offers 164 KB per SM against 7.5's 64 KB.launchbound tune <kernel> --backend modelnow asks for--cc.
--ccis validated at the command line, and the CUDA spellings work. A mistyped--ccused to be handed tocargo reconvergeonce per candidate: eleven subprocesses forreduce-flip, 101 over the corpus, and ninety lines of output in which the actual problem appeared nowhere. It is now one line in ~100ms with nothing spawned.--cc 86and--cc sm_86are normalized to8.6rather than rejected — for two digits the mapping is unambiguous, and it is the spelling a CUDA user already has.
-
A reconverge failure reports what reconverge said. The tool error showed the last six lines of its stderr — a reasonable-looking default, since a failing tool usually fails last, and reliably the wrong six: reconverge prints its diagnosis first and its usage reference after it, so the tail was the exit-code legend. reconverge 0.4.0 stopped printing usage after a bad value, which fixes that case at the source; this reads the lines marked
error:regardless, because no caller controls what its analyzer prints, and falls back to the head rather than the tail. -
modelsays that it is not gated. It ranks the whole space, and onreduce-flipits top five are all configurations the gate refuses — the fastest row was a kernel that hangs, under a header that carefully said "estimated, not a measurement" and nothing about safety. It still runs no gate and needs noreconverge; it now says so, and namestune --backend modelas the gated form. -
tune --backend modelno longer leaves an empty run directory. The directory was created before the backend match, for every backend, and the model path writes nothing — so every run litteredruns/, which is checked in, andlaunchbound reporton it failed withverdicts.json: No such file. An--outgiven to this backend is now answered rather than silently ignored. -
launchbound-tui --helpprints help. It readargs()directly, so every flag was taken as a run-directory path:--helpcame back asrun dir: --help/verdicts.json: No such file or directory, which reads as a broken tool.-h,--help,-Vand--versionanswer; any other leading dash is reported as an unknown option, which is what stops the next flag landing here as a path. This is a published binary. -
The chosen configuration's interval is dropped, not cut. At eighty columns — the default terminal size, and the width this suite mandates — the line ended
0.0400 ms [0.0398,: a number with no upper bound and a dangling comma, on the one line carrying the result. The interval now goes whole when it does not fit; at 110 columns it is unchanged. -
The TUI goldens wait for a finished frame. They synced on a 150ms quiet period, which is a guess at how long a repaint takes; on a loaded runner the app pauses mid-repaint and the screen read is half-painted. This had already cost the suite once —
ranking_scrolls_a_long_candidate_listcarries a comment about a golden blessed from a too-early capture, which then verified nothing while passing — and the same shape failed reconverge'smainon macOS. The binary already brackets every repaint in DEC 2026 synchronized updates, sowait_frameobserves only whole frames. The 100-iteration stress gate went from 15.8s to 0.7s.
- The Action's floating tag is now
@v2.uses: vyncint/launchbound/action@v1keeps working and stays on 1.2.0, which is the point of a floating major tag — but it stays on reconverge 0.3.0 with it, and 0.3.0 is the analyzer that cannot see a named-const shared-memory size. Move to@v2to get the gate this release is about. - Add
--ccto anylaunchbound tuneinvocation. It has no default now.
- The CLI table listed
launchbound tui <run>, which is not a subcommand — the binary islaunchbound-tui. It also omittedmodel, and showedtunewithout the--ccit now requires. - The Action's input table still gave
reconverge-versionas0.1.11, two releases stale.
-
The safety gate now pins
reconverge0.3.0, up from 0.1.11 — two minor versions of analyzer the gate was not getting. The pin moves in four places: the corpus workflow, the Action'sreconverge-versiondefault, and the two documents that name it.The gate admits exactly the same set. A newer analyzer can change what the gate refuses, which is a change in product behaviour rather than a dependency bump, so the corpus was re-run under both versions on the same toolchain and compared: 93 clean, 8 refused, 0 caveats, 0 tool errors, and the two runs are byte-identical — same candidate hashes, same
REFUSED RC001lines, same reasons. The eight refusals arereduce-flipabove one warp, which is the corpus's known flip and the behaviour the gate exists to produce. The measurement is recorded in docs/research-baseline.md.No toolchain change was needed: launchbound and reconverge 0.3.0 already pin the same
nightly-2026-04-03, so the rule that the analyzer and the toolchain move together is satisfied without moving either.What this does not establish is general equivalence. reconverge gained multi-warp replay, bounded inlining and unmasked warp-wrapper analysis between these versions; a kernel exercising those paths could be decided differently. Six kernels are the evidence, and six kernels are what they are.
-
docs/LIMITATIONS.mdnow describes the limits of the analyzer the gate actually runs, and carries the date it was re-checked.
-
The overview shows the field, not just the winner. Under the chosen configuration it now lists every measured candidate fastest-first, with what each costs relative to the fastest. An autotuner's output is not a configuration, it is the claim that the configuration is worth choosing, and that claim is unreadable without the alternatives: a field inside a percent says the tuning did not matter, one spanning 4x says it did.
The percentage is anchored on the fastest measured candidate rather than on the chosen one, deliberately. Where a refused configuration measured faster — the case the overview already warns about in bold — anchoring on the chosen would bury the number that matters. On the
reduce-flipfixture it now reads plainly: the disqualified candidate is fastest and the safe choice costs 90.5%.The list is the head of view 2's, from one shared ordering, so the two cannot disagree. Where it does not fit, it truncates and says how many more view 2 holds.
- The overview fills the pane it was given. It drew four lines into a twenty-five-row box; the rest was blank.
- termlens 0.5 → 0.6 for the TUI test harness. No source change was
needed: the only breaking change in 0.6 is
GraphicsSeenbecomingClonerather thanCopy, and this suite asserts on text. What the upgrade buys is a fix that matters here —openptyis now retried when the machine is briefly out of PTY devices, which macOS is whenever a suite runs one test per core.
- The stress workflow gained a flake hunt. The existing gate is unchanged —
the TUI suite once per OS on every push and pull request. The hunt runs the
same suite many times over, split across five machines that each use a
different
--test-threads, on dispatch or weekly. Five machines because a race that only loses on a slow runner gets five rolls rather than one; five concurrencies because that is the axis a PTY suite's faults live on, and running one point five times only samples that point harder. - A published-package check, at release and weekly:
cargo install launchbound-cliinto a clean directory from crates.io, then real work with no GPU, no network and no checkout — enumerating a config space whose constraint rules out exactly one of six pairs. It is the only check here that can fail without anybody touching the repository, becausecargo installresolves dependencies fresh where CI resolves against the lockfile. - The gate job no longer caches the analyzer. It cached
cargo-reconvergeandreconverge-driverand skipped the install on a hit; a cached binary is not evidence that the gate holds against the published analyzer. Nothing in this repository's CI or its action caches anything now. - Squash merges no longer fail the DCO check. GitHub rewrites a web-flow squash commit's author email after the sign-off is written, so the exact match the check required was impossible by construction. Such commits must still carry a sign-off; every other rule is unchanged.
- Reports with no measurements are labelled
unmeasuredinstead ofmeasured(schema gains the value; a run directory with only a gate record no longer misstates itself). - The release workflow refuses to publish when the pushed tag disagrees with the workspace version, reads the version deterministically, and warns loudly on manual dispatch; the trigger glob is now defense-in-depth rather than the only barrier.
- Every step of the GitHub Action pins
RUSTUP_TOOLCHAIN, so a rust-toolchain.toml in the consumer's checkout can no longer hijack the launchbound install or the gate; the toolchain contract (the gate builds your kernel under the action's toolchain) is documented. A truncated or empty gate record now surfaces the prune failure instead of a parser traceback. - The lockstep pins are written once per workflow (env), the pin-watch issue names every site a bump must touch, CONTRIBUTING gains the release checklist (including the easy-to-forget floating-v1 move), and the action README references the floating tag.
- rustdoc warnings and leftover internal section references cleaned up.
- reconverge (cargo-reconverge + reconverge-driver) installs from its
crates.io releases instead of the git tag, in both the action and
prune.yml; the action input renames to
reconverge-version.
- The safety gate as a composite GitHub Action (
action/):uses: vyncint/launchbound/action@v1prunes a kernel's space in CI with no GPU, with verdict counts, the full gate record, and a job-summary table as outputs. - README badges (license, crates.io, pinned toolchain, GPU-not-required, CI).
- Releases publish through crates.io Trusted Publishing (GitHub OIDC): no registry token is stored anywhere.
- termlens dev-dependency upgraded to 0.5 (fallible
send, identical closed-terminal behaviour across Linux and macOS).
- Project bootstrap: pinned workspace (
nightly-2026-04-03, lockstep withcuda-oxideandreconverge), dual MIT/Apache-2.0 licensing, governance docs, safety-gate specification (docs/SAFETY.md), and CI. launchbound-space: kernel.toml tuning spaces, constraints, deterministic enumeration, canonical config IDs.- Six-kernel device-only corpus with declared spaces, [bench] workloads, [model] shared-memory expressions, and an MSL twin for reduce-stable.
launchbound-prune: the safety gate — one reconverge run per specialization, findings.v1 parsing, and the pure launch-shape decision rule; gate tests reproduce the known-flip/known-stable directions.launchbound-build: scratch specialization, an Apple-container or directcargo oxideexecutor, and a source-hash artifact cache.launchbound-bench+launchbound-runner: plan.v1/results.v1, dlopen'd CUDA driver API, cuEvent timing, interval statistics, checkpoint/resume, CPU heartbeat, and a watchdog for --allow-unsafe candidates.launchbound-search: exhaustive and seeded-random orders, honored budgets, candidates-per-GPU-hour.launchbound-report: report.v1 with the REFUSED-BUT-FASTER section, indistinguishability, and schema + golden validation.launchbound-model: occupancy/wave estimator shipping with measured per-kernel Spearman correlations (model-calibration.toml).launchbound-metal: Apple-Silicon measurement with the mandatory no-gate notice on every surface.launchbound-tui: four deterministic views, termlens goldens, and the 100-iteration stress gate.- CLI verbs: space, prune, stage, tune (cuda/metal/model), report, apply, model.
- docs: ARCHITECTURE, BENCHMARKING, LIMITATIONS, SAFETY, research-baseline.