Skip to content

Commit 4dc6f13

Browse files
committed
docs(pr): amend PR #611 changelog with the post-changelog wave (Waves 1.3–6.1)
Branch advanced 31 substantive commits past the first changelog (which described through 33058b8). The branch was rebased meanwhile — the original changelog commit bb7ed7a8 is orphaned, its twin is 4140f4d, and 33058b8 remains an ancestor of HEAD, so 33058b8..HEAD is the true new-work boundary. Learned and folded in: - np.evaluate — Tier-3C fusion made public; per-node NumPy result_type typing (fixes the mixed-tree dtype bug: i4*i4+f8 must wrap in int32 first), fused reductions, EXTERNAL_LOOP guard, out= via ufunc rules. 3.2–6.1x vs NumPy. - out=/where=/dtype= across the elementwise ufunc API (binary, unary-math, comparisons, predicates, bitwise, invert, arctan2) — one NumPy-shaped overload each, exact broadcast/cast/error-text semantics. - New at np.*: bitwise_and/or/xor (were operator-only, CS0117) and positive. - nditer: WRITEMASKED/ARRAYMASK execution + VIRTUAL operands (was silent masked-write corruption); Wave-1.4 fixes (size-1 stride-0 invariant, op_axes OOB, write-broadcast validation, PARALLEL_SAFE, unit-axis absorb). - Alloc Wave 2.4: buffer-pool window 4KiB–1MiB -> 1B–64MiB, pool-side GC pressure, finalizer suppression. - Canonical NDIter benchmark suite + post-release benchmark.yml CI + DocFX Benchmarks-vs-NumPy website pages; honest frontier findings recorded (broadcast-reduce 54x, scalar np.any 14.5x, BUFFERED+REDUCE ForEach P0 crash, parallel banding 4.7x win). Stats refreshed: 272/519/+198k -> 312 commits, 615 files, +217,949/-16,402. Tests: 9,447 -> 9,709 passed/0 failed (net10.0). New-API count 30 -> 35. Same content (minus H1) pushed live to the PR #611 description via REST PATCH.
1 parent 81622de commit 4dc6f13

1 file changed

Lines changed: 53 additions & 9 deletions

File tree

docs/PR611_CHANGELOG.md

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,65 @@
22

33
**[Major Rewrite] NumPy nditer port, NpyExpr DSL with 3-tier custom-op API, C/F/A/K memory layout support, stride-native matmul**
44

5-
**272 commits · 519 files · +198,407 / −16,069** (vs `master`, after #612)
5+
**312 commits · 615 files · +217,949 / −16,402** (vs `master`, after #612)
66

77
---
88

99
## TL;DR
1010

1111
- **`NpyIter` — full port of NumPy 2.4.2's `nditer`** (~12.5K lines): all iteration orders (C/F/A/K), all indexing modes, buffered casting, buffered-reduce double-loop, masking, memory-overlap protection (`COPY_IF_OVERLAP`), windowed buffering (`DELAY_BUFALLOC`), unlimited operands and dimensions. 566+ byte-for-byte NumPy parity scenarios.
12-
- **`NpyExpr` DSL + three-tier custom-op API** — write your own ufuncs: raw IL (Tier 3A), element-wise scalar/SIMD (Tier 3B), or composable expression trees with operator overloads (Tier 3C). Fused expressions run **2.8–5.4× faster than NumPy** (which can't fuse).
12+
- **`NpyExpr` DSL + three-tier custom-op API** — write your own ufuncs: raw IL (Tier 3A), element-wise scalar/SIMD (Tier 3B), or composable expression trees with operator overloads (Tier 3C). Now reachable as a public API: **`np.evaluate`** runs fused expressions **3.2–6.1× faster than NumPy** (which can't fuse), with per-node NumPy `result_type` typing and fused reductions.
13+
- **`out=` / `where=` / `dtype=` ufunc kwargs across the elementwise API** — present on every NumPy ufunc, previously absent from all of NumSharp; now on the binary, unary-math, comparison, predicate, and bitwise families with exact NumPy broadcast/cast/error-text semantics. Plus new `np.bitwise_and/or/xor` and `np.positive` at the `np.*` surface.
1314
- **NumPy-parity benchmark: geomean 1.00× at 10M elements** across ~409 ops (166 faster / 171 close / 36 slower) — measured by a new official BenchmarkDotNet-vs-NumPy suite committed with the report.
1415
- **30 new `np.*` APIs**`pad` (11 modes), `tile`, `median`/`percentile`/`quantile` (all 13 interpolation methods) + their `nan*` variants, `average`, `ptp`, `take`/`put`/`place`, `extract`/`compress`, `diagonal`/`trace`, `argwhere`/`flatnonzero`, `unravel_index`/`ravel_multi_index`/`indices`, `delete`/`insert`/`append`, `diff`/`ediff1d`, `asfortranarray`/`ascontiguousarray`, `np.multithreading`.
1516
- **C/F/A/K order support wired through the whole API**`Shape` understands F-contiguity, `OrderResolver` resolves NumPy order modes, ~68 layout bugs fixed across 9 fix groups.
1617
- **Stride-native matmul/dot** — BLIS-style GEBP GEMM absorbs arbitrary strides for all dtypes (kills a ~100× penalty on transposed inputs); fused 1-D dot is 3.5–9× faster with zero GC; opt-in multithreaded dot ~2× faster than NumPy's default on 1M vectors.
1718
- **Deterministic memory management** — atomic reference counting + `IDisposable` on `NDArray`, plus a tcache-style sub-1MiB buffer pool.
1819
- **Differential fuzzing infrastructure** — 37,445 bit-exact NumPy-comparison cases across 24 corpus tiers, a seeded random fuzzer with shrinker, a CI FuzzMatrix gate, and a nightly soak workflow.
1920
- **Legacy iterator stack deleted**`MultiIterator` and the Regen-generated `NDIterator` cast templates are gone (−3,870 LOC); `NDIterator` survives as a thin lazy wrapper over `NpyIter`.
20-
- **Test suite: 9,447 passed / 0 failed** on net10.0 — +2,219 new test methods, plus the 37,445-case fuzz corpus replayed by the FuzzMatrix gate.
21+
- **Test suite: 9,709 passed / 0 failed** on net10.0 (+2,500 new test methods), plus the 37,445-case fuzz corpus replayed by the FuzzMatrix gate.
22+
23+
---
24+
25+
## Latest wave (roadmap Waves 1.3–6.1) — added after the first changelog
26+
27+
The 31 commits since the initial changelog turn the proven-but-unreachable machinery into shipped public API, close the ufunc-signature gap with NumPy, and put the iterator under adversarial benchmarking.
28+
29+
### `np.evaluate` — fused expressions as a first-class API
30+
31+
The Tier-3C fusion path (2.8–5.4× in the POC) had **zero production callers**; it's now `np.evaluate(expr[, operands][, out])`.
32+
33+
- **Per-node NumPy `result_type` typing** (`NpyExpr.Typing.cs`) — the old DSL computed every node at the *output* dtype, which is wrong for mixed trees: `(i4*i4)+f8` must wrap the multiply in int32 (→ `1410065408`) before promoting; the old model gave `1e10`. A typing pass now resolves every node to its NumPy 2.4.2 dtype: strong-strong NEP50 (incl. int/float tier crossing), weak python-scalar literals (`i4+2 → i4`, `i4/2 → f8`) with NumPy's exact `OverflowError`, and special resolvers (`true_divide`, `arctan2`, `power`-with-negative-integer-literal `ValueError`, bool `add`=OR/`multiply`=AND).
34+
- **Fused reductions**`NpyExpr.Sum/Prod/Min/Max/Mean` compile a one-pass raw inner loop; `sum(a*b)` reads `a` and `b` once and never materializes the product. NumPy reduction dtypes (int→i64, uint→u64, mean→f64); f16/f32 sums accumulate in f64 (documented divergence).
35+
- **`out=` joins via the ufunc rules** (same_kind validation, reference identity, overlap-safe aliasing through `COPY_IF_OVERLAP`); dtype-mismatched `out` rides the windowed cast flush.
36+
- **EXTERNAL_LOOP guard** — throws instead of silently running the kernel at `count==1` (the measured ~40× foot-gun).
37+
- **Measured** (Release, 4M f64, NumPy 2.4.2): `a*b+c` 3.2×, `(a-b)/(a+b)` 6.1×, `sum(a*b)` 3.6×, `sum f32` 2.9×, `i4*2+f8` 3.5×, `a*b+c out=` 2× — all exceeding the POC window. Permanent gate in `benchmark/poc/evaluate_bench.{cs,py}`.
38+
39+
### `out=` / `where=` / `dtype=` across the elementwise ufunc API
40+
41+
The ufunc kwargs on every NumPy ufunc, absent from NumSharp until now, landed across the elementwise core — **all semantics probed against NumPy 2.4.2 before implementation and pinned verbatim by tests**:
42+
43+
- **Families:** binary (`add`, `subtract`, `multiply`, `divide`, `true_divide`, `mod`, `power`, `floor_divide`), unary-math (`sqrt`, `exp`, `log`, `sin`, `cos`, `tan`, `abs`/`absolute`, `negative`, `square`), the six comparisons, predicates (`isnan`/`isfinite`/`isinf`), bitwise, `invert`, `arctan2`. Each collapsed to **one NumPy-shaped overload** (not a bare+out pair).
44+
- `out` joins the broadcast but **never stretches** (mismatched/stretchable `out` raise NumPy's exact texts, trailing space included); loop dtype resolved from inputs (NEP50), `out` only needs a same_kind cast; provided instance returned (reference identity).
45+
- `where` must be exactly `bool` (NumPy casts the mask under 'safe'); broadcasts over operands **and** participates in output shape; mask-false slots keep prior `out` contents.
46+
- `out` **aliasing an input is well-defined** via the Wave-1.1 `COPY_IF_OVERLAP` machinery — `add(x[:-1], x[:-1], out=x[1:])` matches NumPy exactly.
47+
- `dtype=` **computes in the loop dtype**: `subtract(300, 5, dtype=i16) = 295`; fixed an ordering bug where the bool `add`→OR / `multiply`→AND remap keyed off the *promoted* dtype and ran before the `dtype=` override (`add(True, True, dtype=i32)` now correctly returns `2`, not `1`).
48+
- **New at `np.*`:** `np.bitwise_and` / `np.bitwise_or` / `np.bitwise_xor` (existed only as operators `& | ^` — were `CS0117` despite the docs listing them) and `np.positive` (full ufunc surface).
49+
50+
### Iterator: WRITEMASKED execution + VIRTUAL operands + Wave-1.4 fixes
51+
52+
- **WRITEMASKED / ARRAYMASK now EXECUTE** (Wave 1.3) — a buffered masked write previously landed garbage (`99`s) in exactly the slots NumPy preserves (silent corruption of the elements the caller asked to protect); the window flush now writes back only mask-nonzero elements. **VIRTUAL operands** (null op slots) construct with NumPy 2.x semantics instead of throwing. Architecture settled by probing where NumPy enforces masking (one place: the buffered copy-back) before writing code.
53+
- **Wave-1.4 correctness** — size-1 stride-0 invariant (a `(1,4)` view with nonzero stride corrupted `RemoveMultiIndex` iteration), `op_axes` out-of-bounds read on stretched size-1 operand axes, write-broadcast validation, `PARALLEL_SAFE` wiring, unit-axis absorption — all reproduced against NumPy 2.4.2 first, fixed by adopting NumPy's constructor structure.
54+
55+
### Allocation (Wave 2.4)
56+
57+
The allocator tax was the **pool window, not the machinery**: `SizeBucketedBufferPool`'s 4 KiB–1 MiB window missed both hot patterns — the small-N ufunc result (1000×f32 = 4000 B, 96 B under the floor) and every 4M+ output (16–32 MiB, above the cap) — so the dominant shapes paid a fresh `VirtualAlloc` + demand-zero faults while the pool idled. Window opened to **1 B – 64 MiB** (≥1 MiB buckets capped at 2 buffers), GC memory pressure moved pool-side tracking live state, and `GC.SuppressFinalize` on free pulls freed blocks out of the finalizer queue.
58+
59+
### NpyIter benchmark suite + DocFX website + honest frontier findings
60+
61+
- **Canonical NpyIter benchmark** — one section-addressable harness, 33 op families × {scalar/1K/100K/1M/10M}, integrated into `run_benchmark.py`, plus a new **post-release CI workflow** (`.github/workflows/benchmark.yml`) that auto-commits report cards to master.
62+
- **DocFX website pages**`benchmarks.md` (head-to-head evidence companion to the IL-generation page), `benchmark-iterator.md`, `benchmark-matrix.md`, driven by the auto-committed artifacts.
63+
- **Adversarial frontier probes recorded losses, not just wins** — surfaced and documented (not hidden): `np.sum` over a `broadcast_to` view **54× slower** than NumPy (falls to a coordinate-walking general path at 7.4 ns/elem), scalar `np.any` **14.5× slower** (scalar scan where `count_nonzero` on the same array runs SIMD), a **P0 AccessViolation** crash on `ForEach` over a `BUFFERED+REDUCE` iterator (repro pinned/skipped — only the buffered-reduce driver handles that config), and iterator `ALLOCATE` zeroing outputs where NumPy leaves them empty (+2.33 ms/4M). Wins too: hand-rolled 8-band parallel iteration **4.7×**. These are tracked as the next optimization frontier.
2164

2265
---
2366

@@ -63,7 +106,7 @@ User-extensible kernel layer on top of `NpyIter` — the public answer to "how d
63106
- **Tier 3B — `ExecuteElementWise`**: provide scalar + vector IL; the shell supplies a 4×-unrolled SIMD loop, remainder vector, scalar tail, and strided fallback.
64107
- **Tier 3C — `ExecuteExpression`**: compose `NpyExpr` trees with C# operators (`(a - b) / (a + b)`), 50+ node types (arithmetic, trig, exp/log, rounding, predicates, comparisons, `Min/Max/Clamp/Where`), plus **`Call()`** to splice any delegate/`MethodInfo` into a fused kernel. Compiled once, cached by structural key, ~5 ns dispatch.
65108

66-
This is what powers the 2.8–5.4× fusion wins above — one pass, no temporaries.
109+
This is what powers the 2.8–5.4× fusion wins above — one pass, no temporaries — and is now exposed publicly as **`np.evaluate`** with per-node NumPy typing (see *Latest wave*).
67110

68111
## 3. Legacy iterator stack retired
69112

@@ -82,10 +125,11 @@ This is what powers the 2.8–5.4× fusion wins above — one pass, no temporari
82125

83126
## 5. New & completed `np.*` APIs
84127

85-
**New functions (30):**
128+
**New functions (35):**
86129

87130
| Area | APIs |
88131
|---|---|
132+
| Fused / ufunc | `np.evaluate` (fused expressions), `np.bitwise_and`, `np.bitwise_or`, `np.bitwise_xor`, `np.positive` (+ `out=`/`where=`/`dtype=` across the elementwise API — see *Latest wave*) |
89133
| Manipulation | `np.pad` (all 11 NumPy modes + callable), `np.tile`, `np.delete`, `np.insert`, `np.append` |
90134
| Indexing/selection | `np.take`, `np.put`, `np.place`, `np.extract`, `np.compress`, `np.argwhere`, `np.flatnonzero`, `np.diagonal`, `np.trace`, `np.unravel_index`, `np.ravel_multi_index`, `np.indices` |
91135
| Statistics | `np.median`, `np.percentile`, `np.quantile` (**all 13 interpolation methods**, tuple axis, `out=`, `keepdims`, QuickSelect engine), `np.average` (`weights`, `returned`, tuple-axis; fused kernel 1.3–1.6× faster than NumPy at 1M), `np.ptp`, `np.nanmedian`, `np.nanpercentile`, `np.nanquantile` |
@@ -191,10 +235,10 @@ New `examples/NeuralNetwork.NumSharp`: a 2-layer MLP with a naive implementation
191235

192236
## 16. Tests & CI
193237

194-
- **+2,219 test methods**; suite now **9,447 passed / 0 failed / 11 skipped** on net10.0 (also green on net8.0). Zero regressions maintained commit-by-commit.
195-
- New suites: NpyIter battletests (566 scenarios), order-support sections 41–51, ARC lifecycle, clone regression, np.pad/average/median/percentile/ptp/diff battle tests, IL-kernel battle tests, behavioral audit harness.
196-
- CI: fuzz gate in `build-and-release.yml`, new nightly `fuzz-soak.yml`.
197-
- **Known gaps stay visible**: `np.sort` remains unimplemented (tracked `[OpenBugs]`), and small-N (~1K) dispatch overhead is the documented next focus (`docs/ROADMAP.md`) — every open issue found by the audits/fuzzers is checked in as a failing-by-design `[OpenBugs]` test rather than ignored.
238+
- **+2,500 test methods**; suite now **9,709 passed / 0 failed** on net10.0 (also green on net8.0). Zero regressions maintained commit-by-commit.
239+
- New suites: `np.evaluate` (per-node wraparound, dtype matrices, weak scalars + overflow, fused-vs-unfused, `out=` identity/cast/aliasing, fused reductions), `out=`/`where=`/`dtype=` parity suites (broadcast/cast/error-text pins), WRITEMASKED/VIRTUAL parity; NpyIter battletests (566 scenarios), order-support sections 41–51, ARC lifecycle, clone regression, np.pad/average/median/percentile/ptp/diff battle tests, IL-kernel battle tests, behavioral audit harness.
240+
- CI: fuzz gate in `build-and-release.yml`, nightly `fuzz-soak.yml`, **new post-release `benchmark.yml`** (auto-commits NumPy-comparison report cards to master).
241+
- **Known gaps stay visible**: `np.sort` remains unimplemented (`[OpenBugs]`); the frontier benches' broadcast-reduce (54×), scalar `np.any` (14.5×) losses and the `BUFFERED+REDUCE` `ForEach` P0 crash (pinned/skipped repro) are documented as the next optimization frontier; small-N (~1K) dispatch overhead remains the headline focus (`docs/ROADMAP.md`). Every open issue found by the audits/fuzzers/benches is checked in as a failing-by-design `[OpenBugs]` test or pinned repro rather than ignored.
198242

199243
---
200244

0 commit comments

Comments
 (0)