diff --git a/.github/workflows/pins.yml b/.github/workflows/pins.yml index 4d9a359..0b622a2 100644 --- a/.github/workflows/pins.yml +++ b/.github/workflows/pins.yml @@ -1,15 +1,16 @@ -# Pin watch: reports when cuda-oxide or reconverge -# move upstream by opening an issue. It NEVER bumps a pin — a bump is a human -# decision, its own commit, and a gate re-run (CONTRIBUTING.md). +# Pin watch: reports when cuda-oxide or reconverge move upstream by opening an +# issue. It NEVER bumps a pin — a bump is a human decision, its own commit, +# and a gate re-run (CONTRIBUTING.md). # -# Dispatch-only: the weekly schedule is commented out in `on:` below, so an -# upstream move is noticed when somebody runs this, not when it happens. +# It runs weekly. It did not always: the schedule was commented out, so the +# watch only spoke when somebody dispatched it, and between 2026-08-31 and +# 2026-09-09 upstream moved 133 commits and one toolchain with nothing said. +# A watch that runs when you remember to ask it is a watch you do not have. name: pins on: - # Scheduled runs are disabled. Uncomment the block below to restore them. - # schedule: - # - cron: "17 6 * * 1" # weekly, Monday morning UTC + schedule: + - cron: "17 6 * * 1" # weekly, Monday morning UTC workflow_dispatch: permissions: @@ -19,9 +20,9 @@ permissions: # The lockstep pin set. A pin bump updates these values (and # rust-toolchain.toml) in its own commit. env: - CUDA_OXIDE_PIN: 50d07314eb8b7d5ec821ba02b0048a753c20dd4e + CUDA_OXIDE_PIN: 26754ae52c26c097dc1c465a1e42c4c5d05a3d40 CUDA_OXIDE_REPO: NVlabs/cuda-oxide - RECONVERGE_PIN: v0.5.0 + RECONVERGE_PIN: v0.6.0 RECONVERGE_REPO: vyncint/reconverge jobs: @@ -30,6 +31,14 @@ jobs: env: GH_TOKEN: ${{ github.token }} steps: + # Needed for the toolchain comparison: the nightly we pin lives in + # rust-toolchain.toml, not in this file's env block. + - uses: actions/checkout@v4 + with: + # This job reads one file and talks to `gh api` with the job token; + # it never pushes, so the checkout credential is pure attack surface. + persist-credentials: false + - name: Compare upstreams to pins run: | set -euo pipefail @@ -40,6 +49,25 @@ jobs: drift="$drift- \`$CUDA_OXIDE_REPO\` HEAD is \`${head_sha:0:8}\`, pin is \`${CUDA_OXIDE_PIN:0:8}\`"$'\n' fi + # The toolchain, which is the pin that actually breaks things. A + # commit-count drift is usually docs; a channel move means our + # rustc can no longer build upstream's kernels, and reconverge -- + # a rustc driver -- must be rebuilt against it. Reported as its own + # line so a reader can tell the two apart at a glance, which the + # sha-only report of #1201 did not let anyone do. + ours=$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml) + theirs=$(gh api "repos/$CUDA_OXIDE_REPO/contents/rust-toolchain.toml" \ + -H "Accept: application/vnd.github.raw" | sed -n 's/^channel = "\(.*\)"/\1/p') + # An extractor that silently matches nothing is the same failure as + # a stale pin, one level up: it reports "current" forever. Fail loud. + if [ -z "$ours" ] || [ -z "$theirs" ]; then + echo "could not read a toolchain channel (ours='$ours' theirs='$theirs') -- has rust-toolchain.toml changed shape?" >&2 + exit 1 + fi + if [ "$ours" != "$theirs" ]; then + drift="$drift- \`$CUDA_OXIDE_REPO\` builds on \`$theirs\`, \`rust-toolchain.toml\` pins \`$ours\` **(toolchain move — reconverge must be rebuilt)**"$'\n' + fi + latest_tag=$(gh api "repos/$RECONVERGE_REPO/tags" -q '.[0].name') if [ "$latest_tag" != "$RECONVERGE_PIN" ]; then drift="$drift- \`$RECONVERGE_REPO\` latest tag is \`$latest_tag\`, pin is \`$RECONVERGE_PIN\`"$'\n' @@ -50,6 +78,8 @@ jobs: exit 0 fi + echo "$drift" + title="pins: upstream moved past the lockstep pin set" if gh issue list --repo "$GITHUB_REPOSITORY" --state open \ --search "in:title \"$title\"" --json title -q '.[].title' | grep -qF "$title"; then @@ -57,4 +87,4 @@ jobs: exit 0 fi - gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$(printf 'Weekly pin watch found upstream movement:\n\n%s\nPolicy (CONTRIBUTING.md): the nightly, cuda-oxide, and reconverge pins move together or not at all. A bump is its own commit with its own gate re-run, touching EVERY site: rust-toolchain.toml, .github/workflows/pins.yml (env), .github/workflows/prune.yml (env), action/action.yml (input defaults), action/README.md, and CONTRIBUTING.md. This workflow never bumps — a human decides.' "$drift")" + gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$(printf 'Weekly pin watch found upstream movement:\n\n%s\nPolicy (CONTRIBUTING.md): the nightly, cuda-oxide, and reconverge pins move together or not at all. A bump is its own commit with its own gate re-run, touching EVERY site — `scripts/check-pins.sh` is the list and it fails on disagreement: rust-toolchain.toml, README.md (the toolchain badge), .github/workflows/pins.yml (env), .github/workflows/prune.yml (env), action/action.yml (input defaults), action/README.md, docs/LIMITATIONS.md, corpus/README.md, crates/launchbound-build/src/compile.rs (CUDA_OXIDE_PIN), and CONTRIBUTING.md. This workflow never bumps — a human decides.' "$drift")" diff --git a/.github/workflows/prune.yml b/.github/workflows/prune.yml index 39e3a11..b4c835b 100644 --- a/.github/workflows/prune.yml +++ b/.github/workflows/prune.yml @@ -16,8 +16,8 @@ permissions: # rust-toolchain.toml, and the action defaults (action/action.yml) — the # pins.yml issue body lists every site. env: - PINNED_TOOLCHAIN: nightly-2026-04-03 - RECONVERGE_VERSION: "0.5.0" + PINNED_TOOLCHAIN: nightly-2026-08-28 + RECONVERGE_VERSION: "0.6.0" jobs: gate: @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 with: repository: NVlabs/cuda-oxide - ref: 50d07314eb8b7d5ec821ba02b0048a753c20dd4e + ref: 26754ae52c26c097dc1c465a1e42c4c5d05a3d40 path: cuda-oxide - name: Install pinned toolchain working-directory: launchbound diff --git a/.gitignore b/.gitignore index 96d123f..cb40029 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ target/ # Staged bench plans and box session outputs are artifacts, not source. /plans/ /bench-session/ +# `cargo oxide inspect` writes its lowering beside the kernel it lowered, so +# the command corpus/README.md tells a reader to run leaves the tree dirty. +# These are build outputs; the PTX that matters is recorded, not committed. +corpus/*/*.ptx +corpus/*/*.ll diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e13947f..79fda52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -169,9 +169,10 @@ Three pins move together or not at all: the nightly in wraps — and `cuda-oxide` requires the same pin. A bump is its own commit, never mixed with a behaviour change, and re-runs the affected stage gates. The `pins.yml` workflow reports upstream movement by opening an issue; it -never bumps anything. Its weekly cron is commented out, so upstream movement -is noticed when you dispatch it. Current pins: nightly-2026-04-03, -cuda-oxide 50d07314, reconverge 0.5.0 (installed from crates.io). +never bumps anything. It runs every Monday, and reports a toolchain move as +its own line — a channel change means reconverge must be rebuilt, which +commit churn does not. Current pins: nightly-2026-08-28, +cuda-oxide 26754ae5, reconverge 0.6.0 (installed from crates.io). ## 10. License diff --git a/README.md b/README.md index 6b66da4..95edef8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue)](LICENSE-MIT) [![crates.io](https://img.shields.io/crates/v/launchbound-cli?label=crates.io&color=orange)](https://crates.io/crates/launchbound-cli) -[![toolchain](https://img.shields.io/badge/toolchain-nightly--2026--04--03-orange)](rust-toolchain.toml) +[![toolchain](https://img.shields.io/badge/toolchain-nightly--2026--08--28-orange)](rust-toolchain.toml) [![GPU](https://img.shields.io/badge/GPU-not%20required-brightgreen)](docs/ARCHITECTURE.md) [![CI](https://img.shields.io/github/actions/workflow/status/vyncint/launchbound/ci.yml?label=CI)](https://github.com/vyncint/launchbound/actions/workflows/ci.yml) diff --git a/action/README.md b/action/README.md index 1829e6c..b5c14b5 100644 --- a/action/README.md +++ b/action/README.md @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v4 with: repository: NVlabs/cuda-oxide - ref: 50d07314eb8b7d5ec821ba02b0048a753c20dd4e + ref: 26754ae52c26c097dc1c465a1e42c4c5d05a3d40 path: cuda-oxide - name: Safety gate @@ -63,8 +63,8 @@ the gate specializes per candidate. | `cc` | — | target compute capability, e.g. `"8.6"` (required; verdicts do not transfer across parts) | | `fail-on` | `tool-error` | `never`, `refused`, or `tool-error` | | `version` | `latest` | launchbound-cli release to install | -| `reconverge-version` | `0.5.0` | reconverge release from crates.io — moves in lockstep with `toolchain` | -| `toolchain` | `nightly-2026-04-03` | the nightly that built that reconverge | +| `reconverge-version` | `0.6.0` | reconverge release from crates.io — moves in lockstep with `toolchain` | +| `toolchain` | `nightly-2026-08-28` | the nightly that built that reconverge | | `summary` | `"true"` | write the verdict table to the job summary | ## Outputs diff --git a/action/action.yml b/action/action.yml index 0a9c8a7..0c57031 100644 --- a/action/action.yml +++ b/action/action.yml @@ -51,7 +51,7 @@ inputs: reconverge-driver). Must be built by the toolchain below — the pins move together. required: false - default: "0.5.0" + default: "0.6.0" toolchain: description: >- The pinned nightly that matches reconverge-version; the pair moves @@ -61,7 +61,7 @@ inputs: a different toolchain, pass a matching reconverge-version/toolchain pair. required: false - default: nightly-2026-04-03 + default: nightly-2026-08-28 summary: description: Write the verdict table to the job summary ("true"/"false"). required: false diff --git a/corpus/README.md b/corpus/README.md index ed6216f..a2cfa0a 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -34,5 +34,5 @@ Every kernel is a **standalone, device-only lib crate**: ```bash git clone https://github.com/NVlabs/cuda-oxide ../cuda-oxide # sibling of this repo -git -C ../cuda-oxide checkout 50d07314 # the pinned commit +git -C ../cuda-oxide checkout 26754ae5 # the pinned commit ``` diff --git a/crates/launchbound-build/src/compile.rs b/crates/launchbound-build/src/compile.rs index d7c5e8e..fd14a26 100644 --- a/crates/launchbound-build/src/compile.rs +++ b/crates/launchbound-build/src/compile.rs @@ -158,7 +158,7 @@ fn is_missing_subcommand(stderr: &str) -> bool { /// asserts this constant against `rust-toolchain.toml` and the workflows, /// because drift between recorded pins is the failure this repository keeps /// hitting. -pub const CUDA_OXIDE_PIN: &str = "50d07314eb8b7d5ec821ba02b0048a753c20dd4e"; +pub const CUDA_OXIDE_PIN: &str = "26754ae52c26c097dc1c465a1e42c4c5d05a3d40"; /// What to say when `cargo oxide` is not installed. fn missing_oxide_message() -> String { diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 10b0009..41d174f 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -2,11 +2,13 @@ A tool that overstates its reach is worse than one that does less. These are launchbound's, with numbers where we have them. Everything here was true on -2026-08-22 against the pins in rust-toolchain.toml and CONTRIBUTING.md. +2026-08-22 against the pins in rust-toolchain.toml and CONTRIBUTING.md; the +pin-dependent claims were re-checked on 2026-09-09 against 2.2.0's lockstep +set, and the corpus decided identically ([research-baseline](research-baseline.md)). ## The gate inherits reconverge's limits, wholesale -A clean gate is **not a proof of correctness**. `reconverge` (v0.5.0) is +A clean gate is **not a proof of correctness**. `reconverge` (v0.6.0) is summary-based and interprocedural, handles reducible control flow only, cannot evaluate non-literal masks, and puts data races entirely out of scope. Its own documentation is the authority; launchbound adds no analysis @@ -105,8 +107,12 @@ a T4. ## cuda-oxide is alpha Its README says to expect bugs, incomplete features, and API breakage. The -pins (CONTRIBUTING.md) move together or not at all; both upstreams had already -moved past the pinned versions on the day this was written. cuda-oxide +pins (CONTRIBUTING.md) move together or not at all. 2.2.0's bump put the +cuda-oxide pin *at* upstream `main` (`26754ae5`) rather than behind it, which +is a fact with a shelf life measured in days — `pins.yml` reports the drift +every Monday, and names a toolchain move separately from commit churn. The +previous set had gone 133 commits and one nightly stale precisely because that +watch only ran when somebody dispatched it. cuda-oxide emits `.target sm_80` PTX for this corpus, so `needs_cc = "8.0"` across the board and nothing here runs on pre-Ampere parts. `cargo check` under the reconverge driver does not evaluate all codegen-time consts (an invalid diff --git a/docs/research-baseline.md b/docs/research-baseline.md index beea862..674e01f 100644 --- a/docs/research-baseline.md +++ b/docs/research-baseline.md @@ -172,3 +172,61 @@ are equivalent in general: reconverge gained multi-warp replay, bounded inlining and unmasked warp-wrapper analysis between these versions, and a kernel exercising those paths could well be decided differently. The corpus is the evidence, and the corpus is six kernels. + +## Lockstep bump: nightly-2026-08-28, cuda-oxide `26754ae5`, reconverge 0.6.0 + +The three pins moved together for 2.2.0 (#50). The previous set — +`nightly-2026-04-03`, cuda-oxide `50d07314`, reconverge 0.5.0 — was 133 +commits and one toolchain behind upstream, so the compile path could not +build a kernel written against a current `cuda-device`. + +A pin bump can change **what the gate admits**, so the corpus was run under +both sets and the output diffed, the same method §"Analyzer equivalence" +used. Measured 2026-09-09, tier 0 (no GPU): + +```console +$ cargo run -q -p launchbound-cli -- prune --cc 8.6 # both pin sets +$ diff prune-old-pin.txt prune-new-pin.txt # no output +``` + +| kernel | clean | caveats | refused | tool errors | +|---|---|---|---|---| +| histogram | 12 | 0 | 0 | 0 | +| matmul-tiled | 18 | 0 | 0 | 0 | +| reduce-flip | 3 | 0 | **8** | 0 | +| reduce-stable | 11 | 0 | 0 | 0 | +| scan-block | 4 | 0 | 0 | 0 | +| stencil-1d | 45 | 0 | 0 | 0 | +| **total** | **93** | **0** | **8** | **0** | + +**The two runs differ in no byte** — the same candidate hashes, the same +eight `REFUSED RC001` lines, the same reasons, and the same totals the +0.1.11 → 0.3.0 comparison recorded three weeks earlier. The gate tests pass +unchanged (`known_flip_kernel_disqualifies_above_one_warp`, +`known_stable_kernel_disqualifies_nothing`, `tool_error_is_a_hard_stop`). + +### The compile path, which is the half that was actually broken + +The prune leg never needed the bump — it is analysis only. The compile leg +did: `launchbound-build` shells out to `cargo oxide inspect`, and that +subcommand comes from the checkout the pin names. At the new pin every +corpus kernel lowers to PTX: + +| kernel | `cargo oxide inspect` | PTX lines | `.visible .entry` | `.target` | +|---|---|---|---|---| +| histogram | ok | 109 | 1 | `sm_80` | +| matmul-tiled | ok | 195 | 1 | `sm_80` | +| reduce-flip | ok | 120 | 1 | `sm_80` | +| reduce-stable | ok | 115 | 1 | `sm_80` | +| scan-block | ok | 205 | 1 | `sm_80` | +| stencil-1d | ok | 88 | 1 | `sm_80` | + +`cargo-oxide` 0.2.1, built from the pinned checkout — it is not published to +crates.io, so there is no version to install and the checkout is the only +source. + +**What this does and does not establish.** It establishes that the bump +changes nothing this corpus can observe, and that the compile path works at +the new pin on a machine with LLVM 21 and no CUDA toolkit. It does not +establish that PTX *runs*: `inspect` lowers, it does not execute, and no GPU +was involved in any line of this section. The corpus is still six kernels. diff --git a/justfile b/justfile index a7142d0..4205d08 100644 --- a/justfile +++ b/justfile @@ -41,7 +41,7 @@ gate: prune cc="8.6": cargo run -q -p launchbound-cli -- prune --cc {{ cc }} -# The six recorded pin sites agree with each other. No network: the +# The recorded pin sites agree with each other. No network: the # dispatch-only `pins.yml` asks upstream, this asks ourselves — and a # watcher whose own baseline is stale reports drift from a version nothing # installs, which is how #17 came to describe a pin two releases old. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a14a8c0..1a02996 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -4,9 +4,9 @@ # together or not at all; a bump is its own commit with its own gate re-run. # # Current lockstep set: -# nightly nightly-2026-04-03 -# cuda-oxide 50d07314eb8b7d5ec821ba02b0048a753c20dd4e -# reconverge v0.5.0 +# nightly nightly-2026-08-28 +# cuda-oxide 26754ae52c26c097dc1c465a1e42c4c5d05a3d40 +# reconverge v0.6.0 [toolchain] -channel = "nightly-2026-04-03" +channel = "nightly-2026-08-28" components = ["rustfmt", "clippy"] diff --git a/scripts/check-pins.sh b/scripts/check-pins.sh index 8341c28..d1c30ad 100755 --- a/scripts/check-pins.sh +++ b/scripts/check-pins.sh @@ -85,6 +85,12 @@ toolchain_sites() { # matches nothing is the same failure as a stale pin, one level up. site "action/action.yml" \ "$(sed -n '/^ toolchain:/,/^ [a-z]/s/^ default: "\{0,1\}\(nightly-[0-9-]*\)"\{0,1\}.*/\1/p' action/action.yml)" + # The README badge. It was not a site until 2.2.0, and it drifted exactly + # as an unchecked site does: the badge still advertised nightly-2026-04-03 + # after the pin had moved, which is the first pin a reader sees. shields.io + # escapes a literal hyphen as `--`, so unescape before comparing. + site "README.md" \ + "$(sed -n 's|.*/badge/toolchain-\(nightly[0-9-]*\)-orange.*|\1|p' README.md | sed 's/--/-/g')" } # Read `` lines on stdin; every value must match.