Skip to content

Commit 6414b09

Browse files
docs+feat: honest wiki claims + Dense pair/keyword constructor so docs resolve (#68)
## Summary A **documentation-honesty pass** on the flagship wiki (from a dedicated doc-vs-code audit), plus the one **code change** needed to make the wiki's own examples valid — fixing at source rather than sweeping the symptom. Every claim below was ground-truthed against the code and the repo's own benchmark, *not* taken on the audit's word (some audit findings were over-flagged — see "What the audit got wrong"). ## Docs — no overclaim | Claim (before) | Reality | Fix | |---|---|---| | "2-3× faster than PyTorch" (FAQ/Vision/Migration/Home) | The repo's **own** benchmark measures a **~0.73× geomean** vs PyTorch — slower on average, winning 11/25 ops; PyTorch's MKL wins large element-wise ops 5–25× | State the measured truth, link `benchmark/results_2026-02-20_framework-comparison.md`, and say the differentiator is provable correctness (the benchmark's own conclusion) | | "Rust backend" as the current/optional compute backend | The compute backend is **Zig** (`ZigBackend`); there is **no** `RustBackend` compute path (the only `RustBackend` is in vendored `AcceleratorGateVendored.jl`) | Fix present-tense mislabels → Zig | | `compile(model, backend=:rust/:zig/:julia)` | `compile(; backend::AbstractBackend = JuliaBackend())` — a **symbol** MethodErrors | → `backend=ZigBackend("/path/to/libaxiom_zig.so")` (the README's real idiom) | | "`@ensure`/`@prove` are PROVEN, not just tested" / "mathematical proof" | `prove.jl` itself says its heuristics are "**NOT** symbolic execution and **NOT** formal verification" and returns `:unknown` honestly | Soften to match the code's own disclaimer | | `[Rust Backend](Rust-Backend.md)` | File does not exist | → `Performance-Tuning.md` (real page) | ## Code — make the documented API real (doctrine: fix at source) The docstring (`src/layers/dense.jl:38`) and **dozens** of wiki examples use Flux-style `Dense(in => out)` and keyword `activation=…`, but the constructor was positional-only — so those standalone examples MethodError. Rather than sweep 40+ doc sites, the constructor now accepts what its own docs advertise: - `Dense(in => out)`, `Dense(in => out, relu)`, `Dense(in => out; activation=relu)` (Flux-style pair form) - `Dense(in, out; activation=relu)` (keyword activation) - Positional `Dense(in, out, relu)` still works and behaves **identically**; the keyword wins if both are given. Verified: **0 method ambiguities** (`detect_ambiguities(Axiom; recursive=true) == 0`), forward pass unchanged, new-forms guarded in the `Dense Layer` testset (**11/11 pass**), and the two disagreeing Dense docstrings reconciled. ## What the audit got wrong (and I corrected for) - The audit claimed *all* `Dense(=>)` examples MethodError. In fact, **inside `@axiom` blocks the macro parses layers structurally and accepts both pair-form and keyword-activation** — so the Tutorials build fine. Only **standalone** constructor calls were broken. This constructor change makes both paths correct. ## ⚠️ Two items I deliberately did NOT change — they need your decision 1. **Version maturity contradiction.** `Project.toml` / `src/Axiom.jl` / the benchmark all say **1.0.0**, but `FAQ.md:30` says "alpha (v0.1.0)" and the README roadmap leaves "v1.0 — production ready" **unchecked**. Resolving this is a release-semantics call (bump docs to 1.0, or the tag down to 0.1) — flagged, not touched. 2. **Roadmap "Rust backend" vs shipped Zig.** The forward-looking roadmap (`Vision.md` "The Road Ahead", `Roadmap-Commitments.md` — which declares itself the source of truth — and `Certification-Readiness.md`) commits to a *future* "Full Rust backend", and `Roadmap-Commitments.md:94` cites a non-existent `docs/wiki/Rust-Backend.md`. The **shipped** backend is Zig, and estate policy migrated Rust→Zig. I did **not** rewrite the commitments doc (outward-facing promise; intent is yours). Please decide: align the roadmap to Zig, or keep a genuine Rust-backend commitment (and add the missing `Rust-Backend.md`)? 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UPFC9YQ7g9gc3VnRox42Q1 --- _Generated by [Claude Code](https://claude.ai/code/session_01UPFC9YQ7g9gc3VnRox42Q1)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ed7e24 commit 6414b09

7 files changed

Lines changed: 92 additions & 46 deletions

File tree

docs/wiki/FAQ.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,19 @@ end
226226

227227
### Is Axiom.jl faster than PyTorch?
228228

229-
**Inference**: Yes, 2-3x faster with Zig backend
230-
**Training**: Comparable (Julia) to 1.5x faster (Zig)
229+
**It depends on the operation.** Axiom's own measured benchmark
230+
(`benchmark/results_2026-02-20_framework-comparison.md`) records a geometric mean of
231+
~0.73× PyTorch for the SmartBackend — i.e. slower on average, winning 11 of 25 ops.
232+
It is faster on small inputs and on RMSNorm/LayerNorm/BatchNorm at small sizes, but
233+
PyTorch's MKL wins large element-wise ops (sigmoid/gelu/softmax at ≥100K) by 5–25×.
234+
Axiom's differentiator is provable correctness, not raw speed. Training throughput is
235+
not separately benchmarked.
231236

232237
### How do I enable the Zig backend?
233238

234239
```julia
235240
# Compile for Zig
236-
model = compile(my_model, backend=:zig)
241+
model = compile(my_model, backend=ZigBackend("/path/to/libaxiom_zig.so"))
237242

238243
# Or set environment variable
239244
ENV["AXIOM_BACKEND"] = "zig"

docs/wiki/Framework-Comparison.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121

2222
```julia
2323
# The type system ensures correctness
24-
model = @axiom begin
24+
model = @axiom MnistNet begin
2525
@ensure input_shape == (784,) "MNIST images must be 784-dim"
2626
Dense(784 => 256, activation=relu)
2727
Dense(256 => 10, activation=softmax)
2828
@ensure output_shape == (10,) "Must output 10 classes"
2929
end
3030

31-
# Compile-time verification
31+
# Verification (static where a property is provable; runtime otherwise)
3232
@prove BoundedOutputs(0.0, 1.0) model
3333
```
3434

docs/wiki/Home.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ result = verify(model, properties=[ValidProbabilities(), FiniteOutput()], data=b
126126

127127
## Optional Backends
128128

129-
Axiom is Julia-first. The Rust backend is optional and used only when you
130-
explicitly enable it (e.g., for high-performance kernels or SMT runner
131-
hardening). Most users can ignore it entirely.
129+
Axiom is Julia-first. The optional **Zig backend** provides native SIMD kernels for
130+
high-performance hot paths and is used only when you explicitly enable it
131+
(e.g. `compile(model, backend=ZigBackend(path))`). Most users can ignore it entirely.
132132

133133
---
134134

@@ -139,10 +139,14 @@ hardening). Most users can ignore it entirely.
139139
| Shape checking | Runtime | Runtime | **Compile time** |
140140
| Formal proofs | No | No | **Yes** |
141141
| REPL exploration | No | No | **Yes** |
142-
| Performance | Good | Good | **Better** (Rust) |
142+
| Performance | Good | Good | Competitive (Zig)† |
143143
| Safety certification | No | No | **Yes** |
144144
| Learning curve | Low | Medium | Low |
145145

146+
> *Performance is workload-dependent: Axiom's Zig SmartBackend is competitive with
147+
> PyTorch on small/medium ops and behind on large element-wise ops (geomean ~0.73×).
148+
> See `benchmark/results_2026-02-20_framework-comparison.md`.*
149+
146150
---
147151

148152
## Documentation Map
@@ -160,7 +164,7 @@ hardening). Most users can ignore it entirely.
160164
- [@axiom DSL](Axiom-DSL.md) - The declarative model definition
161165
- [Verification System](Verification.md) - @ensure and @prove
162166
- [Architecture](Architecture.md) - Deep dive into design
163-
- [Rust Backend](Rust-Backend.md) - Performance architecture
167+
- [Performance Tuning](Performance-Tuning.md) - Backend selection and optimization
164168

165169
### API Reference
166170
- [Complete API Reference](API-Reference.md) - All functions, types, macros

docs/wiki/Migration-Guide.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111
|--------------|---------|----------|
1212
| Compile-time shape checking | | |
1313
| Formal verification | | |
14-
| 2-3x faster inference | | |
14+
| Competitive kernel performance† | | |
1515
| REPL exploration | | |
1616
| Keep existing models | | |
1717

18+
> *Performance is workload-dependent — Axiom is competitive with PyTorch on
19+
> small/medium ops and behind on large element-wise ops; see
20+
> `benchmark/results_2026-02-20_framework-comparison.md`. The real differentiator is
21+
> compile-time verification, not raw speed.*
22+
1823
**The best part**: You don't have to rewrite anything. Import and go.
1924

2025
---
@@ -460,12 +465,13 @@ After migration, you can compile for production:
460465
# Development (Julia backend)
461466
dev_model = from_pytorch("model.pytorch.json")
462467

463-
# Production (Zig backend) - 2-3x faster
464-
prod_model = compile(dev_model, backend=:zig, optimize=:aggressive)
468+
# Production (Zig backend)
469+
prod_model = compile(dev_model, backend=ZigBackend("/path/to/libaxiom_zig.so"), optimize=:aggressive)
465470

466-
# Benchmark
467-
@time dev_model(test_input) # 0.012s
468-
@time prod_model(test_input) # 0.004s
471+
# Benchmark on your own workload — op-level medians are in
472+
# benchmark/results_2026-02-20_framework-comparison.md
473+
@time dev_model(test_input)
474+
@time prod_model(test_input)
469475
```
470476

471477
---

docs/wiki/Vision.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -100,31 +100,35 @@ Flatten()(input) |> Dense(784, 10) # ✓ Compiles
100100

101101
# ... layers ...
102102

103-
# These are PROVEN, not just tested
103+
# @ensure adds runtime contracts; @prove attempts to discharge them statically
104104
@ensure sum(output) 1.0
105105
@ensure all(output .>= 0)
106106
@prove ∀x. no_nan(output(x))
107107
end
108108
```
109109

110-
**What this means**: You get mathematical proof that your model has certain properties.
110+
**What this means**: `@ensure` attaches runtime contracts that are checked on every
111+
forward pass; `@prove` attempts to discharge a property *statically* — via
112+
known-pattern heuristics, or an SMT solver when `SMTLib.jl` is loaded — and honestly
113+
returns `:unknown` when it cannot. It is not a blanket claim that every property is
114+
formally proved.
111115

112116
### 3. Production Performance
113117

114118
```julia
115119
# Development: Julia backend (fast iteration)
116-
model = compile(MyModel, backend=:julia)
120+
model = compile(MyModel, backend=JuliaBackend())
117121

118-
# Production: Rust backend (maximum speed)
119-
model = compile(MyModel, backend=:rust, optimize=:aggressive)
120-
# 2-3x faster than PyTorch, with formal guarantees
122+
# Production: Zig backend (native SIMD kernels)
123+
model = compile(MyModel, backend=ZigBackend("/path/to/libaxiom_zig.so"), optimize=:aggressive)
124+
# Competitive with PyTorch on small/medium workloads — see benchmark/ for measured medians
121125
```
122126

123-
**What this means**: No compromise between safety and speed.
127+
**What this means**: verification guarantees without giving up competitive performance.
124128

125129
---
126130

127-
## Why Julia + Rust?
131+
## Why Julia + Zig?
128132

129133
### Why Not Pure Python?
130134

@@ -138,9 +142,9 @@ def broken(x):
138142
# Only crashes at runtime, maybe
139143
```
140144

141-
### Why Not Pure Rust?
145+
### Why Not a Pure Systems Language?
142146

143-
Rust is great for systems programming. But ML research needs:
147+
Systems languages like Zig are great for native kernels. But ML research needs:
144148

145149
- **REPL exploration** - Try ideas instantly
146150
- **Interactive visualization** - Plot results immediately
@@ -156,7 +160,7 @@ Rust is great for systems programming. But ML research needs:
156160
// This is a flow killer for research
157161
```
158162

159-
### Why Julia + Rust?
163+
### Why Julia + Zig?
160164

161165
**Julia for research**:
162166
```julia
@@ -167,11 +171,11 @@ julia> model(randn(4, 10)) # Instant feedback
167171
...
168172
```
169173

170-
**Rust for production**:
174+
**Zig for production**:
171175
```julia
172176
# When you're done experimenting
173-
production_model = compile(model, backend=:rust)
174-
# Single binary, 2-3x faster, memory safe
177+
production_model = compile(model, backend=ZigBackend("/path/to/libaxiom_zig.so"))
178+
# Native SIMD kernels; competitive on small/medium ops (see benchmark/)
175179
```
176180

177181
**Best of both worlds.**
@@ -206,7 +210,7 @@ end
206210
```julia
207211
@axiom Model begin
208212
# ...
209-
@prove ∀x. sum(softmax(x)) == 1.0 # Proven mathematically
213+
@prove ∀x. sum(softmax(x)) == 1.0 # discharged via @prove (known pattern; SMT when SMTLib.jl loaded)
210214
@prove ∀x ε. (ε < δ) stable(f(x), f(x+ε)) # Robustness
211215
end
212216
```

src/layers/dense.jl

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ dense1 = Dense(784, 128)
5656
# A layer with ReLU activation
5757
dense2 = Dense(128, 64, relu)
5858
59+
# Flux-style pair form (equivalent), with a keyword activation
60+
dense2b = Dense(128 => 64, activation=relu)
61+
5962
# A layer without a bias term
6063
dense3 = Dense(64, 10, bias=false)
6164
@@ -72,25 +75,24 @@ mutable struct Dense{T, F} <: AbstractLayer
7275
end
7376

7477
"""
75-
Dense(
76-
in_features::Int,
77-
out_features::Int,
78-
activation::F = identity;
79-
bias::Bool = true,
80-
init::AbstractInitializer = DEFAULT_WEIGHT_INIT,
81-
bias_init::AbstractInitializer = DEFAULT_BIAS_INIT,
82-
dtype::Type{T} = Float32
83-
) where {T, F}
78+
Dense(in_features, out_features, σ = identity; activation = nothing, bias = true,
79+
init = GlorotUniform(), bias_init = Zeros(), dtype = Float32)
80+
Dense(in_features => out_features, σ = identity; kwargs...) # Flux-style pair form
8481
85-
Constructs a `Dense` layer.
82+
Constructs a `Dense` layer. The activation may be supplied positionally (`σ`,
83+
Flux-style) or via the `activation` keyword (the keyword wins if both are given).
84+
The pair form `in => out` forwards to the positional constructor, so the
85+
`Dense(784 => 256)` idiom used throughout the docs resolves to a real method.
8686
8787
Arguments:
8888
- `in_features::Int`: The number of input features this layer expects. Must be positive.
8989
- `out_features::Int`: The number of output features this layer produces. Must be positive.
90-
- `activation::F`: The activation function to apply after the linear transformation.
91-
Can be any Julia function or callable object. Defaults to `identity`.
90+
- `σ`: The activation function to apply after the linear transformation (positional).
91+
Can be any Julia function or callable object. Defaults to `identity`.
9292
9393
Keyword Arguments:
94+
- `activation`: Alternative to the positional `σ`; overrides it when given. Lets you
95+
write `Dense(in, out; activation=relu)` and `Dense(in => out; activation=relu)`.
9496
- `bias::Bool`: If `true`, a bias term `b` is added to the output. Defaults to `true`.
9597
- `init::AbstractInitializer`: The initializer strategy for the `weight` matrix.
9698
Defaults to `DEFAULT_WEIGHT_INIT` (GlorotUniform).
@@ -108,21 +110,33 @@ Throws:
108110
function Dense(
109111
in_features::Int,
110112
out_features::Int,
111-
activation::F = identity;
113+
σ = identity;
114+
activation = nothing,
112115
bias::Bool = true,
113116
init::AbstractInitializer = DEFAULT_WEIGHT_INIT,
114117
bias_init::AbstractInitializer = DEFAULT_BIAS_INIT,
115118
dtype::Type{T} = Float32
116-
) where {T, F}
119+
) where {T}
117120
@assert in_features > 0 "in_features must be positive."
118121
@assert out_features > 0 "out_features must be positive."
119122

123+
# The activation may be supplied positionally (`σ`, Flux-style) or by the
124+
# `activation` keyword (as the docstring and docs advertise); the keyword
125+
# wins when both are given.
126+
act = activation === nothing ? σ : activation
127+
120128
weight = T.(init(in_features, out_features)) # Initializes as (in_features, out_features) matrix
121129
b = bias ? T.(bias_init(out_features)) : nothing # Initializes as (out_features,) vector
122130

123-
Dense{T, F}(weight, b, activation, in_features, out_features)
131+
Dense{T, typeof(act)}(weight, b, act, in_features, out_features)
124132
end
125133

134+
# Flux-style pair form: `Dense(in => out)`, `Dense(in => out, relu)`,
135+
# `Dense(in => out; activation=relu)`. Forwards to the primary constructor so the
136+
# `in => out` idiom used throughout the docs resolves to a real method.
137+
Dense(dims::Pair{<:Integer, <:Integer}, args...; kwargs...) =
138+
Dense(Int(first(dims)), Int(last(dims)), args...; kwargs...)
139+
126140
# forward(d::Dense, x::AbstractTensor) is defined in backends/abstract.jl
127141
# with backend-aware dispatch (routes through Zig/GPU when active).
128142

test/runtests.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ using JSON
4343
y = layer_relu(x)
4444
@test all(y.data .>= 0) # ReLU output
4545

46+
# Flux-style pair form: Dense(in => out) — the idiom used throughout the docs.
47+
layer_pair = Dense(784 => 128)
48+
@test layer_pair.in_features == 784
49+
@test layer_pair.out_features == 128
50+
@test layer_pair.activation === identity
51+
@test size(layer_pair(x)) == (32, 128)
52+
53+
# activation supplied positionally, by keyword, and via the pair form — all agree.
54+
@test Dense(784, 128, activation=relu).activation === relu
55+
@test Dense(784 => 128, relu).activation === relu
56+
@test Dense(784 => 128, activation=relu).activation === relu
57+
@test Dense(784 => 128, bias=false).bias === nothing
58+
4659
# Test shape mismatch
4760
x_wrong_shape = Tensor(randn(Float32, 32, 783)) # Wrong number of features
4861
# Note: The actual error will be a MethodError because the matrix multiplication

0 commit comments

Comments
 (0)