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
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ jobs:
exit 1
fi

- name: Every crate ships both licence texts
run: |
set -euo pipefail
names=$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import sys, json; [print(p["name"]) for p in json.load(sys.stdin)["packages"]]')
for crate in $names; do
files=$(cargo package --list -p "$crate")
for lic in LICENSE-MIT LICENSE-APACHE; do
if ! printf '%s\n' "$files" | grep -qx "$lic"; then
echo "::error::$crate would publish without $lic"
exit 1
fi
done
done
echo "all crates carry LICENSE-MIT and LICENSE-APACHE"

- name: Mint a short-lived registry token from GitHub OIDC
id: auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,63 @@ 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).

## [0.5.1] — 2026-09-16

A patch release: ten defects found by deep-testing the published 0.5.0 as a
downstream consumer of the crates.io artifacts rather than as the repository.
Every fix carries a regression test; no public API changed.

### Fixed

- **`maximum`/`minimum` gave a tie's whole gradient to one operand**, so
every composite built on them was wrong at a tie. `BCEWithLogitsLoss` is
`max(x, 0) - x·t + …`, so at a logit of exactly `0.0` it returned `-t`
instead of `sigmoid(0) - t`, and one SGD step on a balanced batch *raised*
the loss from 0.6931472 to 0.724077. A tie now splits evenly — the
convention `docs/LIMITATIONS.md` already documented. `relu'(0) = 0` is
unchanged: `relu` has its own VJP and never went through `maximum` (#84).
- **`Tensor::from_storage`'s bounds check overflowed `isize`**, panicking
inside a `Result`-returning constructor in debug and *accepting* an
out-of-bounds layout in release, where the first read then indexed far
past the buffer. The check now uses checked arithmetic throughout and
treats any overflow as "cannot be proven in bounds" (#85).
- **Batched `matmul` panicked** with rayon's `chunk_size must not be zero`
when the output had a zero-size dimension, while the rank-2 path returned
the empty tensor. Both return it now; a zero *inner* dimension still
produces the zero-filled result it should (#86).
- **`mean` on an `f64` tensor was scaled by an `f32` reciprocal**, so every
f64 mean carried f32 precision — `mean([1, 2, 3])` came back
`2.0000000596046448`. The division happens in the tensor's own dtype (#87).
- **Adam/AdamW bias correction used one global step counter**, so a
parameter whose gradient arrived late had its first update mis-scaled — two
parameters with identical first gradients moved by 0.100 and 0.074. Each
parameter now carries its own update count, which matters because 0.5.0
made skipping grad-less parameters the normal path (#88).
- **Optimizer state was indexed by position and never grew**, so adding a
parameter through the public `groups_mut()` panicked with an index out of
bounds on the next step. State now grows with its group; a parameter with
no slot yet simply has no history, which is what `None` already meant (#89).
- **`serialize::load` was not atomic.** A load that failed partway left the
module holding checkpoint values for the parameters it had already visited
and original values for the rest — a model from neither source, behind a
typed error. It now decodes and validates every parameter before writing
any of them (#90).
- **`serialize::save` panicked inside safetensors** when a `Module` reported
two parameters under the same name. `named_parameters` is written by hand
in every downstream `Module`, so a duplicate is ordinary caller error and
is now a typed `InvalidArgument` naming it (#91).
- **No published crate carried a licence text.** `LICENSE-MIT` and
`LICENSE-APACHE` existed only at the workspace root, so all twelve 0.5.0
tarballs shipped the licence *claim* without the licence. Both files now
live in every crate, and the release workflow refuses to publish one that
would omit them (#92).
- **Documentation described a release that no longer exists.** README and
`docs/LIMITATIONS.md` still named the `ctor` pre-`main` constructor that
0.5.0 deleted and restated 0.4.0's "seven crates, 47 → 40" — measured now,
three crates leave and it is 43 → 40. `SECURITY.md` still said "Nothing is
released yet" for twelve published crates, and the README's optim row named
`SGD`/`RMSprop`, which are spelled `Sgd` and `RmsProp` (#93).

## [0.5.0] — 2026-09-16

The production-hardening milestone (#45–#76): the panics, silent wrong
Expand Down
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 12 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ members = [
exclude = ["research"]

[workspace.package]
version = "0.5.0"
version = "0.5.1"
edition = "2024"
rust-version = "1.88"
readme = "README.md"
Expand All @@ -36,17 +36,17 @@ toml = "0.9"

# Internal crates. The version field is required so path dependencies
# survive `cargo publish`.
oxmera = { path = "crates/oxmera", version = "0.5.0" }
oxmera-core = { path = "crates/oxmera-core", version = "0.5.0" }
oxmera-tensor = { path = "crates/oxmera-tensor", version = "0.5.0" }
oxmera-ops = { path = "crates/oxmera-ops", version = "0.5.0" }
oxmera = { path = "crates/oxmera", version = "0.5.1" }
oxmera-core = { path = "crates/oxmera-core", version = "0.5.1" }
oxmera-tensor = { path = "crates/oxmera-tensor", version = "0.5.1" }
oxmera-ops = { path = "crates/oxmera-ops", version = "0.5.1" }
# default-features off at the workspace level so the facade can forward its
# own `cuda` feature through rather than inheriting an unconditional one.
# Every consumer opts in explicitly; there is exactly one (the facade).
oxmera-runtime = { path = "crates/oxmera-runtime", version = "0.5.0", default-features = false }
oxmera-cpu = { path = "crates/oxmera-cpu", version = "0.5.0" }
oxmera-metal = { path = "crates/oxmera-metal", version = "0.5.0" }
oxmera-cuda = { path = "crates/oxmera-cuda", version = "0.5.0" }
oxmera-autograd = { path = "crates/oxmera-autograd", version = "0.5.0" }
oxmera-nn = { path = "crates/oxmera-nn", version = "0.5.0" }
oxmera-optim = { path = "crates/oxmera-optim", version = "0.5.0" }
oxmera-runtime = { path = "crates/oxmera-runtime", version = "0.5.1", default-features = false }
oxmera-cpu = { path = "crates/oxmera-cpu", version = "0.5.1" }
oxmera-metal = { path = "crates/oxmera-metal", version = "0.5.1" }
oxmera-cuda = { path = "crates/oxmera-cuda", version = "0.5.1" }
oxmera-autograd = { path = "crates/oxmera-autograd", version = "0.5.1" }
oxmera-nn = { path = "crates/oxmera-nn", version = "0.5.1" }
oxmera-optim = { path = "crates/oxmera-optim", version = "0.5.1" }
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ let logits = model.forward(&x)?;
| devices | CPU (rayon-parallel, cache-tiled GEMM), Apple Metal (MSL compute kernels, threadgroup reductions, tiled GEMM over unified memory) and NVIDIA CUDA (the same kernels in CUDA C, shipped as PTX and driven through the driver API — no CUDA toolkit needed to build, `libcuda` found at runtime); `tensor.to_device(...)` moves data, autograd flows across the move |
| autograd | tape-based reverse mode: `requires_grad`, `backward()`, gradient accumulation, `no_grad` RAII guard — every VJP validated by finite differences in CI |
| nn | `Linear`, `Conv2d`, `Embedding`, `LayerNorm`, `BatchNorm2d`, `Dropout`, `Sequential`; `MSELoss`, `CrossEntropyLoss`, `BCEWithLogitsLoss`; Kaiming/Xavier initializers |
| optim | `SGD` (momentum), `Adam` and `AdamW` with per-group learning rate and weight decay (`ParamGroup`); decay-free `RMSprop` with per-group learning rate; `Adam`/`AdamW` take one fused launch per parameter on Metal and CUDA |
| optim | `Sgd` (momentum), `Adam` and `AdamW` with per-group learning rate and weight decay (`ParamGroup`); decay-free `RmsProp` with per-group learning rate; `Adam`/`AdamW` take one fused launch per parameter on Metal and CUDA |
| linalg | `eye`/`diag`/`diag_embed`/`trace`, batched `cholesky` (differentiable), `logdet`/`det`, `eigh`; rank-4+ matmul broadcasting and a two-operand `einsum` |
| weights | zero-config `safetensors` save/load by parameter name |
| terminal | `oxmera doctor` (hardware, devices, capabilities) and `oxmera train --tui` (live loss/accuracy sparklines, progress gauges, throughput, unified-memory usage) — both golden-tested through a real PTY with a 100-iteration determinism stress |
Expand All @@ -55,8 +55,8 @@ oxmera train --tui --device cuda # … or on an NVIDIA GPU

The CUDA backend is on by default. If you will never use it —
Apple Silicon, or a CPU-only box — `cargo add oxmera --no-default-features`
drops it along with `cudarc`, `libloading`, the `ctor` pair, the embedded
PTX and a pre-`main` constructor: seven crates out of the graph.
drops it along with `cudarc`, `libloading` and the embedded PTX: three
crates out of the graph, 43 → 40.
`Device::Cuda` stays a typed runtime error either way, so nothing that
names it stops compiling.

Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

## Supported versions

Nothing is released yet. When releases exist, only the latest published
version of each crate is supported.
Only the latest published version of each crate is supported. Fixes land on
`main` and ship in the next release; there are no backports to earlier
versions.

## Reporting a vulnerability

Expand Down
Loading