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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/advisories.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A fresh security advisory filed against a dependency we already ship is
# not caught by push/PR CI, which runs only when the tree changes. This
# runs the advisory database against the committed lockfile on a schedule,
# so a new CVE surfaces as a failed run within the week rather than at the
# next unrelated push. Bans/licenses/sources stay on the push/PR firewall
# in ci.yml; this leg is advisories only.
name: advisories

on:
schedule:
- cron: "23 6 * * 2" # weekly, Tuesday 06:23 UTC
workflow_dispatch:

permissions:
contents: read

jobs:
advisories:
name: security advisories (cargo-deny)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
with:
command: check advisories
37 changes: 23 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,25 +81,17 @@ jobs:
run: |
set -euo pipefail
tree=$(cargo tree -p oxmera --no-default-features -e normal --prefix none)
# Only the crates that are CUDA-only on EVERY platform. `ctor` is
# not one of them: oxmera-metal registers at load time the same
# way, so on macOS it stays in the graph and should — the first
# version of this step listed it and failed the macOS leg while
# the code was correct.
for crate in cudarc libloading oxmera-cuda; do
# CUDA-only crates that must never appear with the default
# features off. `ctor` joins them since 0.5.0: the Metal and CUDA
# backends no longer register from a pre-main constructor, so no
# crate in the workspace pulls `ctor` on any platform.
for crate in cudarc libloading oxmera-cuda ctor; do
if echo "$tree" | grep -qE "^${crate} "; then
echo "::error::${crate} is still in the graph with --no-default-features"
exit 1
fi
done
echo "cudarc, libloading and oxmera-cuda are all absent"

# `ctor` on macOS belongs to Metal; anywhere else it could only
# have come from CUDA, so check it where the answer is unambiguous.
if [ "$RUNNER_OS" != "macOS" ] && echo "$tree" | grep -qE "^ctor "; then
echo "::error::ctor is still in the graph, and off macOS only CUDA pulls it"
exit 1
fi
echo "cudarc, libloading, oxmera-cuda and ctor are all absent"

msrv:
name: msrv (1.88, lockfile)
Expand Down Expand Up @@ -135,3 +127,20 @@ jobs:
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
with:
command: check bans licenses sources advisories

# The published API cannot break incompatibly without the version saying
# so. cargo-semver-checks compares the workspace against the last release
# on crates.io; a 0.x minor bump is allowed to break, a patch is not.
# Installed from the published crate (not a prebuilt binary), uncached.
semver:
name: semver-checks (public API)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: stable
- name: Install cargo-semver-checks
run: cargo install cargo-semver-checks --locked
- name: The public API change matches the version bump
run: cargo semver-checks --workspace
8 changes: 8 additions & 0 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ on:
permissions:
contents: read

# One gate run per ref: a new push supersedes an in-flight run rather than
# letting both burn a runner installing a rustc-driver from source.
concurrency:
group: gate-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
reconverge-strict:
name: reconverge strict (cuda kernels)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- name: Pinned nightly with driver components
Expand All @@ -46,6 +53,7 @@ jobs:
prune:
name: launchbound prune (cc ${{ matrix.cc }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
cc: ["7.5", "8.6"]
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ jobs:
- name: Verify the tree the tag points at
run: cargo check --workspace --all-targets --locked

- name: The whole suite must pass before anything is published
run: cargo test --workspace --locked

- name: CHANGELOG names the version being released
run: |
set -euo pipefail
version="${TAG#v}"
if ! grep -qE "^## \[${version}\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no '## [${version}]' heading for this release"
exit 1
fi

- name: Mint a short-lived registry token from GitHub OIDC
id: auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1
Expand Down
102 changes: 101 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,67 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.5.0] — 2026-09-16

The production-hardening milestone (#45–#76): the panics, silent wrong
answers and untyped failures a downstream consumer could still hit became
typed errors, the public API was locked down against accidental breakage,
and the release pipeline gained the checks that keep a bad build from
shipping. Breaking changes are collected below with their one-line
migrations; the new `semver-checks` job enforces that a future break
cannot land in a patch.

### Added

- **Fallible tensor constructors** `Tensor::try_zeros`, `try_ones` and
`try_full`, returning a typed error when a shape's element count
overflows `usize`. The infallible `zeros`/`ones`/`full` remain and now
document that they panic on such a shape (#51).
- **`Module::to_device`**, backed by `Param::to_device`, moves a model's
parameters to a device once. Layers read their parameters each forward,
so the per-forward transfer becomes a no-op afterwards and the
device-resident fused Adam step — until now unreachable, because
parameters never left the CPU — finally runs (#46).
- **`Sequential` inspection**: `iter`, `get` and `Index<usize>` over its
children, and a `Debug` that shows them (#61).
- **Optimizer and layer hyperparameters**: `Adam`/`AdamW::with_betas` and
`with_eps`, `RmsProp::with_alpha` and `with_eps`, `LayerNorm::with_eps`,
and `BatchNorm2d::with_eps`/`with_momentum`. Defaults are unchanged (#54).
- **`oxmera doctor --json`** for scripts, and `--help` on both `doctor`
and `train`, which used to be rejected as an unknown argument (#58).
- **`docs/STABILITY.md`**: the SemVer, MSRV and `#[non_exhaustive]`
contract and the road to 1.0 (#45).
- crates.io **keywords and categories** on every crate; a **`semver-checks`
CI job** against the last release; and a **weekly advisory workflow**
running the RustSec database against the committed lockfile, so a new CVE
surfaces without a push (#59, #75). The release workflow now runs the
whole test suite and verifies the CHANGELOG names the version before
publishing (#66), and the convergence gate got a concurrency group and
per-job timeouts (#67).

### Changed

- **`Module` now requires `Debug`.** Breaking. Every layer in the crate
already derived it; an out-of-tree `Module` adds `#[derive(Debug)]`.
This is what lets `Sequential` show its children (#61).
- **`Dropout::new` returns `Result`.** Breaking. It rejects a drop
probability outside `[0, 1)`, which previously scaled survivors by
`1/(1 - p) <= 0` and produced NaNs; the fix at a call site is `?` (#74).
- **`MatmulPlan`, `AdamStep` and `ParamGroup` are `#[non_exhaustive]`.**
Breaking for out-of-tree code that built them with a struct literal or
matched them exhaustively: construct through the constructors
(`AdamStep::new` is new) and add `..` to a destructure (#59).
- **Metal and CUDA register explicitly, not before `main`.** Breaking
behaviour change. The `#[ctor]` constructor and the `ctor` dependency are
gone, along with CUDA's pre-main `dlopen` of `libcuda`. Call
`oxmera::init()` (or `oxmera_cuda::register_default()`) before a GPU
device; the CLI and examples already do. Only device 0 is registered —
`Device::Cuda`/`Metal` with `index > 0` is a typed `BackendUnavailable`,
now documented (#55, #65).
- **`Tensor`'s `Debug` prints metadata** — shape, dtype, device,
requires_grad — not the entire storage buffer, which a derived `Debug`
dumped into every log line and panic message (#62).

- **`Linear`, `Conv2d` and `Embedding` no longer implement `Clone`.**
Breaking. `Param` is a shared handle by design, so deriving `Clone` on a
layer produced a copy that trained together with the original: setting a
Expand Down Expand Up @@ -89,6 +146,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
into the job summary; a new `skill-version` job fails when the vendored
agent skill drifts from the dependency.

### Fixed

- **`Tensor::from_storage` accepted an out-of-bounds layout.** A negative
stride with too small an offset addressed before the storage and
panicked on the first read; the check now accounts for the minimum
addressed index as well as the maximum, and still admits a valid flip
view (#50).
- **`narrow` overflowed `start + len`** in its own bounds check on a huge
range; it now reports a typed error instead (#51).
- **`eigh` rounded an `f64` input to `f32`** before decomposing it, despite
a comment claiming it kept the precision. It now stays in `f64` end to
end (#52).
- **`CrossEntropyLoss` returned `NaN` on an empty batch** (`0 * inf`) and
accepted a target whose length did not match the batch; both are typed
errors now (#47).
- **`MSELoss` and `BCEWithLogitsLoss` accepted a broadcastable target**,
silently computing a wrong loss; the target must match the input shape
exactly (#48).
- **`Conv2d` panicked on an input smaller than its kernel, or a zero
stride** — a `usize` underflow or a divide by zero — instead of
returning a typed error (#49).
- **`BatchNorm2d`'s running variance was biased.** The running estimate now
uses the unbiased sample variance (matching PyTorch) while the current
batch is still normalized with the biased variance (#64).
- **`argmax` hid `NaN`**, skipping it and pointing at an arbitrary element;
a `NaN` input is now a typed error (#74).
- **`Optimizer::step` failed the whole step when one parameter had no
gradient**; it now skips that parameter, as PyTorch does (#53).
- **GPU kernels truncated tensor extents, strides and offsets to `u32`**
with an unchecked cast; an out-of-range value is a typed error before
dispatch, not a wrong-but-plausible launch (#63).
- **`train --tui` panicked (exit 101) when stdout was not a terminal**; it
now fails with a usage-style error and a non-zero exit (#56).
- **Ctrl-C typed into the dashboard was ignored** — only `q` and `Esc`
quit. In raw mode it arrives as a key, which the event loop now treats
like the others (#57).

### Performance

- **CPU matmul borrows contiguous operands** rather than copying both on
every call; a strided or broadcast operand is still gathered, which is
the case that needs it (#73).

## [0.4.0] — 2026-09-06

The audit milestone (#33–#40): what the framework claimed, checked against
Expand Down
Loading