refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
// nisps/ml/dynamic_storage.hpp — runtime-shaped storage policy for the MLP
|
|
|
|
|
// core. WASM / native-test / VCV targets ONLY.
|
|
|
|
|
//
|
|
|
|
|
// Dimensions are chosen at construction; every buffer lives in ONE arena
|
|
|
|
|
// allocated once in the constructor. There is NO allocation after
|
|
|
|
|
// construction — the algorithm hot paths are as allocation-free as the
|
|
|
|
|
// fixed model.
|
|
|
|
|
//
|
|
|
|
|
// This header is compile-time excluded from embedded (RP2350) builds: the
|
|
|
|
|
// firmware's zero-heap contract is structural, not advisory. lint-cpp.sh
|
|
|
|
|
// additionally allowlists exactly this file for its heap audit — heap use
|
|
|
|
|
// anywhere else under nisps/ml/ still fails the lint.
|
|
|
|
|
//
|
|
|
|
|
// See nisps/ml/storage.hpp for the storage surface contract, and
|
|
|
|
|
// docs/specs/plans/one-core-engine-refactor.md §P2 for the design.
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include "../core/perf.hpp"
|
|
|
|
|
|
|
|
|
|
#if defined(NISPS_TARGET_EMBEDDED)
|
|
|
|
|
#error "nisps/ml/dynamic_storage.hpp must not be compiled for the RP2350 target (zero-heap contract)"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include <cstddef>
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
#include <cstdint>
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
#include <new>
|
|
|
|
|
#include <span>
|
|
|
|
|
|
fix(ml): one named example capacity; train() and trainAsync() no longer diverge
Phase 2, S35. Two real defects from one root cause, both confirmed by trace
rather than taken from the audit:
1. Divergence. WasmIML built its TS Dataset mirror with a cap of 100 while
every addExample() ALSO pushed into the C++ FIFO ring, capped at 128. Since
train() reads the C++ ring and trainAsync() reads the TS mirror, past 100
examples the two trained on different datasets — silently.
2. Latent OOB read. nisps_ml_train sizes its sample-weight span by the C++
side's example_count() (up to 128), but wasm-iml.ts allocates that heap
buffer from the TS dataset's size (<=100). Once the ring exceeds the mirror,
the span reads past the end of the caller's allocation.
Fix: name the capacity ONCE as nisps::ml::kDefaultMaxExamples = 128, used by
FixedStorage's default template arg, DynamicStorage's default ctor arg, and the
MLP<> alias (which is the only real FixedStorage instantiation path and carried
its own independent 128 literal — the last copy of this dual truth). Expose it
through nisps_ml_describe and have the TS side read it instead of hardcoding.
Dataset's constructor default is removed entirely: a default was what invited
this bug class, and the sole call site now always supplies the describe() value.
ABI NOTE: this extends nisps_ml_describe from a 6-int to a 7-int descriptor.
nisps_ml_describe always writes 7 ints regardless of the caller's buffer, so
every call site had to grow in the same change or it would overflow the WASM
heap by 4 bytes per call. All five sites updated: three in wasm-iml.ts (init
defaults, init per-instance, reshape re-describe — the finding said there were
two), one in wasm-worker.ts, one in tests/cpp/parity_wasm.mjs. The parity
harness's expected-dims check now also pins the new max_examples slot.
Regression test: tests/cpp/test_mlp_storage_defaults.cpp — pins the two storage
policies to one constant, and drives MLPCore<DynamicStorage> exactly as
bindings.cpp does past the old TS cap, asserting it saturates at 128 and not at
100. Fail-before/pass-after confirmed by temporarily setting the constant to
100: 2 failures, named. Reverted: green.
Audit correction: the cited dataset.ts:81 is the FIFO eviction check; the
hardcoded default was at dataset.ts:45.
Gates: run-all-tests.sh ALL GREEN, parity PASS.
2026-07-21 13:22:38 +02:00
|
|
|
#include "storage.hpp" // kMlpNumLayers, kDefaultMaxExamples
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
|
|
|
|
|
namespace nisps::ml {
|
|
|
|
|
|
|
|
|
|
class DynamicStorage {
|
|
|
|
|
public:
|
|
|
|
|
static constexpr std::size_t kNumLayers = kMlpNumLayers;
|
|
|
|
|
|
|
|
|
|
// hidden must have exactly 3 entries (the 4-layer topology is fixed;
|
|
|
|
|
// only the dimensions are runtime). All dims must be >= 1.
|
|
|
|
|
DynamicStorage(std::size_t n_in,
|
|
|
|
|
std::span<const std::size_t> hidden,
|
|
|
|
|
std::size_t n_out,
|
fix(ml): one named example capacity; train() and trainAsync() no longer diverge
Phase 2, S35. Two real defects from one root cause, both confirmed by trace
rather than taken from the audit:
1. Divergence. WasmIML built its TS Dataset mirror with a cap of 100 while
every addExample() ALSO pushed into the C++ FIFO ring, capped at 128. Since
train() reads the C++ ring and trainAsync() reads the TS mirror, past 100
examples the two trained on different datasets — silently.
2. Latent OOB read. nisps_ml_train sizes its sample-weight span by the C++
side's example_count() (up to 128), but wasm-iml.ts allocates that heap
buffer from the TS dataset's size (<=100). Once the ring exceeds the mirror,
the span reads past the end of the caller's allocation.
Fix: name the capacity ONCE as nisps::ml::kDefaultMaxExamples = 128, used by
FixedStorage's default template arg, DynamicStorage's default ctor arg, and the
MLP<> alias (which is the only real FixedStorage instantiation path and carried
its own independent 128 literal — the last copy of this dual truth). Expose it
through nisps_ml_describe and have the TS side read it instead of hardcoding.
Dataset's constructor default is removed entirely: a default was what invited
this bug class, and the sole call site now always supplies the describe() value.
ABI NOTE: this extends nisps_ml_describe from a 6-int to a 7-int descriptor.
nisps_ml_describe always writes 7 ints regardless of the caller's buffer, so
every call site had to grow in the same change or it would overflow the WASM
heap by 4 bytes per call. All five sites updated: three in wasm-iml.ts (init
defaults, init per-instance, reshape re-describe — the finding said there were
two), one in wasm-worker.ts, one in tests/cpp/parity_wasm.mjs. The parity
harness's expected-dims check now also pins the new max_examples slot.
Regression test: tests/cpp/test_mlp_storage_defaults.cpp — pins the two storage
policies to one constant, and drives MLPCore<DynamicStorage> exactly as
bindings.cpp does past the old TS cap, asserting it saturates at 128 and not at
100. Fail-before/pass-after confirmed by temporarily setting the constant to
100: 2 failures, named. Reverted: green.
Audit correction: the cited dataset.ts:81 is the FIFO eviction check; the
hardcoded default was at dataset.ts:45.
Gates: run-all-tests.sh ALL GREEN, parity PASS.
2026-07-21 13:22:38 +02:00
|
|
|
std::size_t max_examples = kDefaultMaxExamples,
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
std::size_t max_iter_train = 4096u) noexcept {
|
|
|
|
|
if (hidden.size() != 3u || n_in == 0u || n_out == 0u ||
|
|
|
|
|
hidden[0] == 0u || hidden[1] == 0u || hidden[2] == 0u) {
|
|
|
|
|
return; // stays invalid
|
|
|
|
|
}
|
|
|
|
|
dims_[0] = n_in;
|
|
|
|
|
dims_[1] = hidden[0];
|
|
|
|
|
dims_[2] = hidden[1];
|
|
|
|
|
dims_[3] = hidden[2];
|
|
|
|
|
dims_[4] = n_out;
|
|
|
|
|
max_ex_ = max_examples;
|
|
|
|
|
max_iter_ = max_iter_train;
|
|
|
|
|
|
|
|
|
|
std::size_t total = 0u;
|
|
|
|
|
auto claim = [&total](std::size_t n) {
|
|
|
|
|
const std::size_t off = total;
|
|
|
|
|
total += n;
|
|
|
|
|
return off;
|
|
|
|
|
};
|
|
|
|
|
for (std::size_t l = 0; l < kNumLayers; ++l) {
|
|
|
|
|
off_w_[l] = claim(fan_in(l) * fan_out(l));
|
|
|
|
|
off_b_[l] = claim(fan_out(l));
|
|
|
|
|
off_pa_[l] = claim(fan_out(l));
|
|
|
|
|
off_a_[l] = claim(fan_out(l));
|
|
|
|
|
off_gw_[l] = claim(fan_in(l) * fan_out(l));
|
|
|
|
|
off_gb_[l] = claim(fan_out(l));
|
fix(ml): port RMSProp — ported learning rates were landing in SGD
Upstream memlp (github.com/MusicallyEmbodiedML/memlp @ ea777502, the commit
upstream/main pins) applies gradients with RMSProp everywhere: Layer.h:239
ApplyAccumulatedGradients, the m_sq_grad_avg running average at Layer.h:601,
StaticMLP.h:268. nisps/ml/training.hpp shipped SGD only and filed the
difference as an optimiser-choice research question. It was not one.
RMSProp divides each step by the running gradient magnitude, so an upstream
lr is a NORMALISED step; under SGD the same number multiplies the raw
gradient. Every learning rate ported from upstream therefore landed in an
optimiser that reads it differently — most visibly feedback.hpp's
`geo_lr_ = 0.001f // upstream InterfaceRL.hpp:312`, an RMSProp LR pasted
into a single SGD step.
rmsprop_step() ports Layer.h:239 exactly: clip at +/-10, sq = min(0.9*sq +
0.1*g^2, 1e6), adj = min(lr/(sqrt(sq)+1e-6), 1.0), w -= adj*g. The
adjusted-LR clamp stays one-sided as upstream's std::min is, so the negative
lr used by train_targets' "train away from this target" path behaves as it
does upstream. The per-weight squared-gradient average is new persistent
state and lives in the storage policies (FixedStorage arrays /
DynamicStorage arena) so nisps/ stays allocation-free and the firmware's
zero-heap contract holds. It is optimiser state, not model state: excluded
from weight_count()/get_weights()/set_weights(), matching upstream, and
cleared by MLPCore::reset_optimizer_state() (upstream ResetOptimizerState).
draw_weights() deliberately does NOT clear it — upstream's DrawWeights
doesn't either.
Measured with tests/cpp/ml_bench.cpp:
D1 one geometric dislike moves the mapping 1.6e-2, up from 5.3e-5 (~295x),
and repeated presses now CONVERGE on the intended 0.5 push (0.12 at 10,
0.56 at 100) instead of creeping linearly forever.
A4 geometric-vs-Diffuse gap narrows from ~4100x to ~14x in one press.
U4 the upstream-LR positive path actually trains now (range_util 0.71 at
100 ticks/gesture, was 0.016 — it was inert under SGD).
Not fixed by this, and now tracked as ALIGNMENT defect 6d: the dose
asymmetry. lurch_max is still ~1.08 against a [0,1] output range.
Golden vector stages 2 and 3 re-captured; stages 0 and 1 are pre-training
and did not move, which is the cross-check that only the update rule
changed. manifold/public/nisps.wasm rebuilt so parity-check compares like
with like — it FAILED at up to 5e-2 against the stale artifact and PASSES at
2.4e-7 against a fresh one. parity-check.sh only builds the WASM when it is
missing, never when it is stale; noted in MAP.md and filed separately.
ALIGNMENT defect 6 resolved (moved to Recently resolved); 6b's optimiser
cross-reference updated; new defect 6d for the positive-training dose.
Gates: build-cpp-tests 138 tests / ctest 4/4, parity-check PASS, lint-cpp
clean, manifold typecheck clean.
2026-07-25 11:11:23 +02:00
|
|
|
off_sw_[l] = claim(fan_in(l) * fan_out(l));
|
|
|
|
|
off_sb_[l] = claim(fan_out(l));
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
off_d_[l] = claim(fan_in(l));
|
|
|
|
|
off_e_[l] = claim(fan_out(l));
|
|
|
|
|
}
|
|
|
|
|
off_input_ = claim(dims_[0]);
|
|
|
|
|
off_output_ = claim(dims_[4]);
|
|
|
|
|
off_dsf_ = claim(max_ex_ * dims_[0]);
|
|
|
|
|
off_dsl_ = claim(max_ex_ * dims_[4]);
|
|
|
|
|
off_flat_ = claim(weight_count());
|
|
|
|
|
off_lh_ = claim(max_iter_);
|
|
|
|
|
|
|
|
|
|
// The single arena allocation. Value-init zeroes it, matching the
|
|
|
|
|
// zero-initialised std::array members of FixedStorage.
|
|
|
|
|
arena_ = new (std::nothrow) float[total]();
|
|
|
|
|
total_ = (arena_ != nullptr) ? total : 0u;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~DynamicStorage() { delete[] arena_; }
|
|
|
|
|
|
|
|
|
|
DynamicStorage(const DynamicStorage&) = delete;
|
|
|
|
|
DynamicStorage& operator=(const DynamicStorage&) = delete;
|
|
|
|
|
DynamicStorage(DynamicStorage&& o) noexcept { move_from_(o); }
|
|
|
|
|
DynamicStorage& operator=(DynamicStorage&& o) noexcept {
|
|
|
|
|
if (this != &o) {
|
|
|
|
|
delete[] arena_;
|
|
|
|
|
move_from_(o);
|
|
|
|
|
}
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool valid() const noexcept { return arena_ != nullptr; }
|
|
|
|
|
|
|
|
|
|
// ---- dims -----------------------------------------------------------
|
|
|
|
|
std::size_t n_in() const noexcept { return dims_[0]; }
|
|
|
|
|
std::size_t n_out() const noexcept { return dims_[4]; }
|
|
|
|
|
std::size_t max_examples() const noexcept { return max_ex_; }
|
|
|
|
|
std::size_t max_iter_train() const noexcept { return max_iter_; }
|
|
|
|
|
|
|
|
|
|
std::size_t fan_in(std::size_t l) const noexcept { return dims_[l]; }
|
|
|
|
|
std::size_t fan_out(std::size_t l) const noexcept { return dims_[l + 1u]; }
|
|
|
|
|
|
|
|
|
|
template <std::size_t L> std::size_t fan_in_l() const noexcept { return dims_[L]; }
|
|
|
|
|
template <std::size_t L> std::size_t fan_out_l() const noexcept { return dims_[L + 1u]; }
|
|
|
|
|
|
|
|
|
|
std::size_t weight_count() const noexcept {
|
|
|
|
|
return dims_[0] * dims_[1] + dims_[1] * dims_[2] + dims_[2] * dims_[3] +
|
|
|
|
|
dims_[3] * dims_[4] + dims_[1] + dims_[2] + dims_[3] + dims_[4];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- per-layer buffers ------------------------------------------------
|
|
|
|
|
template <std::size_t L> std::span<float> weights_l() noexcept {
|
|
|
|
|
return {arena_ + off_w_[L], fan_in_l<L>() * fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<const float> weights_l() const noexcept {
|
|
|
|
|
return {arena_ + off_w_[L], fan_in_l<L>() * fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> biases_l() noexcept {
|
|
|
|
|
return {arena_ + off_b_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<const float> biases_l() const noexcept {
|
|
|
|
|
return {arena_ + off_b_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> pre_act_l() noexcept {
|
|
|
|
|
return {arena_ + off_pa_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> act_l() noexcept {
|
|
|
|
|
return {arena_ + off_a_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<const float> act_l() const noexcept {
|
|
|
|
|
return {arena_ + off_a_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> grad_w_l() noexcept {
|
|
|
|
|
return {arena_ + off_gw_[L], fan_in_l<L>() * fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> grad_b_l() noexcept {
|
|
|
|
|
return {arena_ + off_gb_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
fix(ml): port RMSProp — ported learning rates were landing in SGD
Upstream memlp (github.com/MusicallyEmbodiedML/memlp @ ea777502, the commit
upstream/main pins) applies gradients with RMSProp everywhere: Layer.h:239
ApplyAccumulatedGradients, the m_sq_grad_avg running average at Layer.h:601,
StaticMLP.h:268. nisps/ml/training.hpp shipped SGD only and filed the
difference as an optimiser-choice research question. It was not one.
RMSProp divides each step by the running gradient magnitude, so an upstream
lr is a NORMALISED step; under SGD the same number multiplies the raw
gradient. Every learning rate ported from upstream therefore landed in an
optimiser that reads it differently — most visibly feedback.hpp's
`geo_lr_ = 0.001f // upstream InterfaceRL.hpp:312`, an RMSProp LR pasted
into a single SGD step.
rmsprop_step() ports Layer.h:239 exactly: clip at +/-10, sq = min(0.9*sq +
0.1*g^2, 1e6), adj = min(lr/(sqrt(sq)+1e-6), 1.0), w -= adj*g. The
adjusted-LR clamp stays one-sided as upstream's std::min is, so the negative
lr used by train_targets' "train away from this target" path behaves as it
does upstream. The per-weight squared-gradient average is new persistent
state and lives in the storage policies (FixedStorage arrays /
DynamicStorage arena) so nisps/ stays allocation-free and the firmware's
zero-heap contract holds. It is optimiser state, not model state: excluded
from weight_count()/get_weights()/set_weights(), matching upstream, and
cleared by MLPCore::reset_optimizer_state() (upstream ResetOptimizerState).
draw_weights() deliberately does NOT clear it — upstream's DrawWeights
doesn't either.
Measured with tests/cpp/ml_bench.cpp:
D1 one geometric dislike moves the mapping 1.6e-2, up from 5.3e-5 (~295x),
and repeated presses now CONVERGE on the intended 0.5 push (0.12 at 10,
0.56 at 100) instead of creeping linearly forever.
A4 geometric-vs-Diffuse gap narrows from ~4100x to ~14x in one press.
U4 the upstream-LR positive path actually trains now (range_util 0.71 at
100 ticks/gesture, was 0.016 — it was inert under SGD).
Not fixed by this, and now tracked as ALIGNMENT defect 6d: the dose
asymmetry. lurch_max is still ~1.08 against a [0,1] output range.
Golden vector stages 2 and 3 re-captured; stages 0 and 1 are pre-training
and did not move, which is the cross-check that only the update rule
changed. manifold/public/nisps.wasm rebuilt so parity-check compares like
with like — it FAILED at up to 5e-2 against the stale artifact and PASSES at
2.4e-7 against a fresh one. parity-check.sh only builds the WASM when it is
missing, never when it is stale; noted in MAP.md and filed separately.
ALIGNMENT defect 6 resolved (moved to Recently resolved); 6b's optimiser
cross-reference updated; new defect 6d for the positive-training dose.
Gates: build-cpp-tests 138 tests / ctest 4/4, parity-check PASS, lint-cpp
clean, manifold typecheck clean.
2026-07-25 11:11:23 +02:00
|
|
|
// RMSProp running squared-gradient averages (training.hpp). Optimiser
|
|
|
|
|
// state, not model state: excluded from weight_count()/copy_weights_to().
|
|
|
|
|
template <std::size_t L> std::span<float> sq_grad_w_l() noexcept {
|
|
|
|
|
return {arena_ + off_sw_[L], fan_in_l<L>() * fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> sq_grad_b_l() noexcept {
|
|
|
|
|
return {arena_ + off_sb_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
template <std::size_t L> std::span<float> delta_l() noexcept {
|
|
|
|
|
return {arena_ + off_d_[L], fan_in_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
template <std::size_t L> std::span<float> eval_act_l() const noexcept {
|
|
|
|
|
return {arena_ + off_e_[L], fan_out_l<L>()};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- global buffers ---------------------------------------------------
|
|
|
|
|
std::span<float> input_buf() noexcept { return {arena_ + off_input_, dims_[0]}; }
|
|
|
|
|
std::span<const float> input_buf() const noexcept { return {arena_ + off_input_, dims_[0]}; }
|
|
|
|
|
std::span<float> output_buf() noexcept { return {arena_ + off_output_, dims_[4]}; }
|
|
|
|
|
std::span<const float> output_buf() const noexcept { return {arena_ + off_output_, dims_[4]}; }
|
|
|
|
|
std::span<float> ds_features() noexcept { return {arena_ + off_dsf_, max_ex_ * dims_[0]}; }
|
|
|
|
|
std::span<const float> ds_features() const noexcept { return {arena_ + off_dsf_, max_ex_ * dims_[0]}; }
|
|
|
|
|
std::span<float> ds_labels() noexcept { return {arena_ + off_dsl_, max_ex_ * dims_[4]}; }
|
|
|
|
|
std::span<const float> ds_labels() const noexcept { return {arena_ + off_dsl_, max_ex_ * dims_[4]}; }
|
|
|
|
|
std::span<float> flat_buf() noexcept { return {arena_ + off_flat_, weight_count()}; }
|
|
|
|
|
std::span<float> loss_hist_buf() noexcept { return {arena_ + off_lh_, max_iter_}; }
|
|
|
|
|
std::span<const float> loss_hist_buf() const noexcept { return {arena_ + off_lh_, max_iter_}; }
|
|
|
|
|
|
refactor(nisps): delete dead core/ML mass; keep the legacy feedback modes
Phase 1 group 2 (L27, L26, L28, S21, L13, ST6, S20).
- L27: fixed_buffer.hpp + its test + the CMake entry — no consumers.
- L26: dislike_multiplier_ and its doubling/halving bookkeeping — upstream
InterfaceRL residue that drove nothing. The audit pointed at the wrong test
file for the surviving reference; the actual assert was in
test_mlp_geo_dislike.cpp:211, removed here.
- L28: added copy_weights_to(std::span<float>) to FixedStorage and
DynamicStorage and switched feedback.hpp's take_snapshot/push_undo/nudge to
it. Drops the permanent whole-net flat_ scratch buffer from FixedStorage and
the per-gesture double copy. Behaviour-identical: same source values, same
write order, same RNG draw order in nudge().
- S21 + L13: deleted NISPS_AUDIO_MEM / NISPS_APP_SRAM / NISPS_AUDIO_FUNC —
zero use sites outside perf.hpp and comments — and rewrote midi_io.hpp's one
misshapen NISPS_AUDIO_FUNC use as a plain `inline void`. perf.hpp now
documents only the inlining/hotness macros that actually exist, and
audio_driver.hpp no longer claims an SRAM discipline the code never had.
- ST6: feedback.hpp's header now describes the four current modes and the
Geometric default, dropping the retracted "geometric push NOT ported" claim.
S20 — OPERATOR DECISION (§7.1): the four legacy feedback behaviours
(RandomiseOutputs, RandomiseMlp, AvoidStyle::Diffuse, the RandomiseMlp branch of
on_drag) are KEPT, not deleted. They are wanted as building blocks for
experimenting with how different instruments feel under different behaviours.
Each is now marked at its definition as deliberately-retained research reserve
so future audits stop flagging it as dead code.
L25 (the 16 KB firmware loss-history buffer) is NOT done here — see the phase
report; it turned out to be coupled into the shared mlp.hpp, and its fate
belongs with the browser telemetry build (§7.3 / plan §6.5e).
Gates: run-all-tests.sh ALL GREEN.
2026-07-21 12:48:27 +02:00
|
|
|
// Copies the live weights+biases directly into `dst` in the same flat
|
|
|
|
|
// layout as MLPCore::get_weights() (weights layer-major, then biases
|
|
|
|
|
// layer-major) — see FixedStorage::copy_weights_to for the rationale.
|
|
|
|
|
void copy_weights_to(std::span<float> dst) const noexcept {
|
|
|
|
|
std::size_t k = 0u;
|
|
|
|
|
for (float v : weights_l<0u>()) dst[k++] = v;
|
|
|
|
|
for (float v : weights_l<1u>()) dst[k++] = v;
|
|
|
|
|
for (float v : weights_l<2u>()) dst[k++] = v;
|
|
|
|
|
for (float v : weights_l<3u>()) dst[k++] = v;
|
|
|
|
|
for (float v : biases_l<0u>()) dst[k++] = v;
|
|
|
|
|
for (float v : biases_l<1u>()) dst[k++] = v;
|
|
|
|
|
for (float v : biases_l<2u>()) dst[k++] = v;
|
|
|
|
|
for (float v : biases_l<3u>()) dst[k++] = v;
|
|
|
|
|
}
|
|
|
|
|
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
private:
|
|
|
|
|
void move_from_(DynamicStorage& o) noexcept {
|
|
|
|
|
for (std::size_t i = 0; i < 5u; ++i) dims_[i] = o.dims_[i];
|
|
|
|
|
max_ex_ = o.max_ex_; max_iter_ = o.max_iter_;
|
|
|
|
|
for (std::size_t l = 0; l < kNumLayers; ++l) {
|
|
|
|
|
off_w_[l] = o.off_w_[l]; off_b_[l] = o.off_b_[l];
|
|
|
|
|
off_pa_[l] = o.off_pa_[l]; off_a_[l] = o.off_a_[l];
|
|
|
|
|
off_gw_[l] = o.off_gw_[l]; off_gb_[l] = o.off_gb_[l];
|
fix(ml): port RMSProp — ported learning rates were landing in SGD
Upstream memlp (github.com/MusicallyEmbodiedML/memlp @ ea777502, the commit
upstream/main pins) applies gradients with RMSProp everywhere: Layer.h:239
ApplyAccumulatedGradients, the m_sq_grad_avg running average at Layer.h:601,
StaticMLP.h:268. nisps/ml/training.hpp shipped SGD only and filed the
difference as an optimiser-choice research question. It was not one.
RMSProp divides each step by the running gradient magnitude, so an upstream
lr is a NORMALISED step; under SGD the same number multiplies the raw
gradient. Every learning rate ported from upstream therefore landed in an
optimiser that reads it differently — most visibly feedback.hpp's
`geo_lr_ = 0.001f // upstream InterfaceRL.hpp:312`, an RMSProp LR pasted
into a single SGD step.
rmsprop_step() ports Layer.h:239 exactly: clip at +/-10, sq = min(0.9*sq +
0.1*g^2, 1e6), adj = min(lr/(sqrt(sq)+1e-6), 1.0), w -= adj*g. The
adjusted-LR clamp stays one-sided as upstream's std::min is, so the negative
lr used by train_targets' "train away from this target" path behaves as it
does upstream. The per-weight squared-gradient average is new persistent
state and lives in the storage policies (FixedStorage arrays /
DynamicStorage arena) so nisps/ stays allocation-free and the firmware's
zero-heap contract holds. It is optimiser state, not model state: excluded
from weight_count()/get_weights()/set_weights(), matching upstream, and
cleared by MLPCore::reset_optimizer_state() (upstream ResetOptimizerState).
draw_weights() deliberately does NOT clear it — upstream's DrawWeights
doesn't either.
Measured with tests/cpp/ml_bench.cpp:
D1 one geometric dislike moves the mapping 1.6e-2, up from 5.3e-5 (~295x),
and repeated presses now CONVERGE on the intended 0.5 push (0.12 at 10,
0.56 at 100) instead of creeping linearly forever.
A4 geometric-vs-Diffuse gap narrows from ~4100x to ~14x in one press.
U4 the upstream-LR positive path actually trains now (range_util 0.71 at
100 ticks/gesture, was 0.016 — it was inert under SGD).
Not fixed by this, and now tracked as ALIGNMENT defect 6d: the dose
asymmetry. lurch_max is still ~1.08 against a [0,1] output range.
Golden vector stages 2 and 3 re-captured; stages 0 and 1 are pre-training
and did not move, which is the cross-check that only the update rule
changed. manifold/public/nisps.wasm rebuilt so parity-check compares like
with like — it FAILED at up to 5e-2 against the stale artifact and PASSES at
2.4e-7 against a fresh one. parity-check.sh only builds the WASM when it is
missing, never when it is stale; noted in MAP.md and filed separately.
ALIGNMENT defect 6 resolved (moved to Recently resolved); 6b's optimiser
cross-reference updated; new defect 6d for the positive-training dose.
Gates: build-cpp-tests 138 tests / ctest 4/4, parity-check PASS, lint-cpp
clean, manifold typecheck clean.
2026-07-25 11:11:23 +02:00
|
|
|
off_sw_[l] = o.off_sw_[l]; off_sb_[l] = o.off_sb_[l];
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
off_d_[l] = o.off_d_[l]; off_e_[l] = o.off_e_[l];
|
|
|
|
|
}
|
|
|
|
|
off_input_ = o.off_input_; off_output_ = o.off_output_;
|
|
|
|
|
off_dsf_ = o.off_dsf_; off_dsl_ = o.off_dsl_;
|
|
|
|
|
off_flat_ = o.off_flat_; off_lh_ = o.off_lh_;
|
|
|
|
|
arena_ = o.arena_; total_ = o.total_;
|
|
|
|
|
o.arena_ = nullptr; o.total_ = 0u;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::size_t dims_[5] = {0u, 0u, 0u, 0u, 0u};
|
|
|
|
|
std::size_t max_ex_ = 0u;
|
|
|
|
|
std::size_t max_iter_ = 0u;
|
|
|
|
|
std::size_t off_w_[kNumLayers]{}, off_b_[kNumLayers]{}, off_pa_[kNumLayers]{},
|
|
|
|
|
off_a_[kNumLayers]{}, off_gw_[kNumLayers]{}, off_gb_[kNumLayers]{},
|
fix(ml): port RMSProp — ported learning rates were landing in SGD
Upstream memlp (github.com/MusicallyEmbodiedML/memlp @ ea777502, the commit
upstream/main pins) applies gradients with RMSProp everywhere: Layer.h:239
ApplyAccumulatedGradients, the m_sq_grad_avg running average at Layer.h:601,
StaticMLP.h:268. nisps/ml/training.hpp shipped SGD only and filed the
difference as an optimiser-choice research question. It was not one.
RMSProp divides each step by the running gradient magnitude, so an upstream
lr is a NORMALISED step; under SGD the same number multiplies the raw
gradient. Every learning rate ported from upstream therefore landed in an
optimiser that reads it differently — most visibly feedback.hpp's
`geo_lr_ = 0.001f // upstream InterfaceRL.hpp:312`, an RMSProp LR pasted
into a single SGD step.
rmsprop_step() ports Layer.h:239 exactly: clip at +/-10, sq = min(0.9*sq +
0.1*g^2, 1e6), adj = min(lr/(sqrt(sq)+1e-6), 1.0), w -= adj*g. The
adjusted-LR clamp stays one-sided as upstream's std::min is, so the negative
lr used by train_targets' "train away from this target" path behaves as it
does upstream. The per-weight squared-gradient average is new persistent
state and lives in the storage policies (FixedStorage arrays /
DynamicStorage arena) so nisps/ stays allocation-free and the firmware's
zero-heap contract holds. It is optimiser state, not model state: excluded
from weight_count()/get_weights()/set_weights(), matching upstream, and
cleared by MLPCore::reset_optimizer_state() (upstream ResetOptimizerState).
draw_weights() deliberately does NOT clear it — upstream's DrawWeights
doesn't either.
Measured with tests/cpp/ml_bench.cpp:
D1 one geometric dislike moves the mapping 1.6e-2, up from 5.3e-5 (~295x),
and repeated presses now CONVERGE on the intended 0.5 push (0.12 at 10,
0.56 at 100) instead of creeping linearly forever.
A4 geometric-vs-Diffuse gap narrows from ~4100x to ~14x in one press.
U4 the upstream-LR positive path actually trains now (range_util 0.71 at
100 ticks/gesture, was 0.016 — it was inert under SGD).
Not fixed by this, and now tracked as ALIGNMENT defect 6d: the dose
asymmetry. lurch_max is still ~1.08 against a [0,1] output range.
Golden vector stages 2 and 3 re-captured; stages 0 and 1 are pre-training
and did not move, which is the cross-check that only the update rule
changed. manifold/public/nisps.wasm rebuilt so parity-check compares like
with like — it FAILED at up to 5e-2 against the stale artifact and PASSES at
2.4e-7 against a fresh one. parity-check.sh only builds the WASM when it is
missing, never when it is stale; noted in MAP.md and filed separately.
ALIGNMENT defect 6 resolved (moved to Recently resolved); 6b's optimiser
cross-reference updated; new defect 6d for the positive-training dose.
Gates: build-cpp-tests 138 tests / ctest 4/4, parity-check PASS, lint-cpp
clean, manifold typecheck clean.
2026-07-25 11:11:23 +02:00
|
|
|
off_sw_[kNumLayers]{}, off_sb_[kNumLayers]{},
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
off_d_[kNumLayers]{}, off_e_[kNumLayers]{};
|
|
|
|
|
std::size_t off_input_ = 0u, off_output_ = 0u, off_dsf_ = 0u, off_dsl_ = 0u,
|
|
|
|
|
off_flat_ = 0u, off_lh_ = 0u;
|
|
|
|
|
float* arena_ = nullptr;
|
|
|
|
|
std::size_t total_ = 0u;
|
|
|
|
|
};
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Runtime-sized feedback-controller storage (see nisps/ml/feedback.hpp for
|
|
|
|
|
// the surface contract). One arena allocation at construction; nothing
|
|
|
|
|
// per-call. The focus mask lives in a byte region carved from the same
|
|
|
|
|
// arena (aliased through the float arena's tail, kept byte-aligned by
|
|
|
|
|
// allocating whole floats for it).
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
class DynamicFeedbackStorage {
|
|
|
|
|
public:
|
|
|
|
|
DynamicFeedbackStorage(std::size_t n_out,
|
|
|
|
|
std::size_t n_weights,
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
std::size_t undo_depth = 4u,
|
|
|
|
|
std::size_t n_in = 2u,
|
|
|
|
|
std::size_t replay_cap = 64u) noexcept
|
|
|
|
|
: n_out_(n_out), n_weights_(n_weights), undo_cap_(undo_depth),
|
|
|
|
|
n_in_(n_in), replay_cap_(replay_cap) {
|
|
|
|
|
if (n_out == 0u || n_weights == 0u || undo_depth == 0u ||
|
|
|
|
|
n_in == 0u || replay_cap == 0u) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// float regions: static_out, placed_out, snapshot, scratch, undo ring,
|
|
|
|
|
// replay (inputs/actions/rewards), centroid + target scratch
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
// byte region: focus mask (n_out bytes, rounded up to whole floats)
|
|
|
|
|
const std::size_t focus_floats = (n_out + sizeof(float) - 1u) / sizeof(float);
|
|
|
|
|
const std::size_t total = n_out * 2u // static_out + placed_out
|
|
|
|
|
+ n_weights * 2u // snapshot + scratch
|
|
|
|
|
+ n_weights * undo_depth // undo ring
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
+ replay_cap * n_in // replay inputs
|
|
|
|
|
+ replay_cap * n_out // replay actions
|
|
|
|
|
+ replay_cap // replay rewards
|
|
|
|
|
+ n_out * 2u // centroid + target
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
+ focus_floats;
|
|
|
|
|
arena_ = new (std::nothrow) float[total]();
|
|
|
|
|
if (!arena_) return;
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
off_placed_ = n_out_;
|
|
|
|
|
off_snap_ = off_placed_ + n_out_;
|
|
|
|
|
off_scratch_ = off_snap_ + n_weights_;
|
|
|
|
|
off_undo_ = off_scratch_ + n_weights_;
|
|
|
|
|
off_replay_in_ = off_undo_ + n_weights_ * undo_cap_;
|
|
|
|
|
off_replay_a_ = off_replay_in_ + replay_cap_ * n_in_;
|
|
|
|
|
off_replay_r_ = off_replay_a_ + replay_cap_ * n_out_;
|
|
|
|
|
off_centroid_ = off_replay_r_ + replay_cap_;
|
|
|
|
|
off_target_ = off_centroid_ + n_out_;
|
|
|
|
|
off_focus_ = off_target_ + n_out_;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~DynamicFeedbackStorage() { delete[] arena_; }
|
|
|
|
|
|
|
|
|
|
DynamicFeedbackStorage(const DynamicFeedbackStorage&) = delete;
|
|
|
|
|
DynamicFeedbackStorage& operator=(const DynamicFeedbackStorage&) = delete;
|
|
|
|
|
DynamicFeedbackStorage(DynamicFeedbackStorage&& o) noexcept { move_from_(o); }
|
|
|
|
|
DynamicFeedbackStorage& operator=(DynamicFeedbackStorage&& o) noexcept {
|
|
|
|
|
if (this != &o) {
|
|
|
|
|
delete[] arena_;
|
|
|
|
|
move_from_(o);
|
|
|
|
|
}
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool valid() const noexcept { return arena_ != nullptr; }
|
|
|
|
|
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
std::size_t n_out() const noexcept { return n_out_; }
|
|
|
|
|
std::size_t n_weights() const noexcept { return n_weights_; }
|
|
|
|
|
std::size_t undo_cap() const noexcept { return undo_cap_; }
|
|
|
|
|
std::size_t n_in() const noexcept { return n_in_; }
|
|
|
|
|
std::size_t replay_cap() const noexcept { return replay_cap_; }
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
|
|
|
|
std::span<float> static_out() noexcept { return {arena_, n_out_}; }
|
|
|
|
|
std::span<const float> static_out() const noexcept { return {arena_, n_out_}; }
|
|
|
|
|
std::span<float> placed_out() noexcept { return {arena_ + off_placed_, n_out_}; }
|
|
|
|
|
std::span<const float> placed_out() const noexcept { return {arena_ + off_placed_, n_out_}; }
|
|
|
|
|
std::span<float> snapshot() noexcept { return {arena_ + off_snap_, n_weights_}; }
|
|
|
|
|
std::span<const float> snapshot() const noexcept { return {arena_ + off_snap_, n_weights_}; }
|
|
|
|
|
std::span<float> scratch_buf() noexcept { return {arena_ + off_scratch_, n_weights_}; }
|
|
|
|
|
std::span<float> undo_slot(std::size_t i) noexcept {
|
|
|
|
|
return {arena_ + off_undo_ + i * n_weights_, n_weights_};
|
|
|
|
|
}
|
|
|
|
|
std::span<const float> undo_slot(std::size_t i) const noexcept {
|
|
|
|
|
return {arena_ + off_undo_ + i * n_weights_, n_weights_};
|
|
|
|
|
}
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
std::span<float> replay_inputs() noexcept { return {arena_ + off_replay_in_, replay_cap_ * n_in_}; }
|
|
|
|
|
std::span<float> replay_actions() noexcept { return {arena_ + off_replay_a_, replay_cap_ * n_out_}; }
|
|
|
|
|
std::span<float> replay_rewards() noexcept { return {arena_ + off_replay_r_, replay_cap_}; }
|
|
|
|
|
std::span<float> centroid_buf() noexcept { return {arena_ + off_centroid_, n_out_}; }
|
|
|
|
|
std::span<float> target_buf() noexcept { return {arena_ + off_target_, n_out_}; }
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
std::span<std::uint8_t> focus() noexcept {
|
|
|
|
|
return {reinterpret_cast<std::uint8_t*>(arena_ + off_focus_), n_out_};
|
|
|
|
|
}
|
|
|
|
|
std::span<const std::uint8_t> focus() const noexcept {
|
|
|
|
|
return {reinterpret_cast<const std::uint8_t*>(arena_ + off_focus_), n_out_};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
void move_from_(DynamicFeedbackStorage& o) noexcept {
|
|
|
|
|
n_out_ = o.n_out_; n_weights_ = o.n_weights_; undo_cap_ = o.undo_cap_;
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
n_in_ = o.n_in_; replay_cap_ = o.replay_cap_;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
off_placed_ = o.off_placed_; off_snap_ = o.off_snap_;
|
|
|
|
|
off_scratch_ = o.off_scratch_; off_undo_ = o.off_undo_; off_focus_ = o.off_focus_;
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
off_replay_in_ = o.off_replay_in_; off_replay_a_ = o.off_replay_a_;
|
|
|
|
|
off_replay_r_ = o.off_replay_r_; off_centroid_ = o.off_centroid_;
|
|
|
|
|
off_target_ = o.off_target_;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
arena_ = o.arena_;
|
|
|
|
|
o.arena_ = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
std::size_t n_out_ = 0u, n_weights_ = 0u, undo_cap_ = 0u, n_in_ = 0u, replay_cap_ = 0u;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
std::size_t off_placed_ = 0u, off_snap_ = 0u, off_scratch_ = 0u,
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
off_undo_ = 0u, off_focus_ = 0u, off_replay_in_ = 0u,
|
|
|
|
|
off_replay_a_ = 0u, off_replay_r_ = 0u, off_centroid_ = 0u,
|
|
|
|
|
off_target_ = 0u;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
float* arena_ = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
refactor(ml)!: P2.1 storage-policy split — MLPCore<Storage>, fixed + dynamic models
Algorithms (forward, backprop/SGD, init, move_weights, diagnostics) now live
once in MLPCore<Storage> (nisps/ml/mlp.hpp). Storage models:
- FixedStorage (storage.hpp): template-sized std::array, zero heap. The
classic MLP<NIn,H1,H2,H3,NOut,...> is an alias preserving kInput/kHidden*/
kOutput/kNumLayers/weight_count() constexpr — firmware + bindings + modes
compile unchanged.
- DynamicStorage (dynamic_storage.hpp): runtime dims, ONE arena allocation
at construction, nothing per-call. #error under NISPS_TARGET_EMBEDDED
(new macro in core/perf.hpp); sole lint-cpp.sh heap-allowlist entry, plus
a lint check that fails if the #error guard disappears.
Verification:
- new ctest test_mlp_storage_parity: fixed↔dynamic BIT-identical across
init/draw/inference/train(FIFO)/move_weights(pin mask)/eval_loss/
layer_stats/set_weights/infer_batch/reset; invalid+moved-from inert
- golden ML vectors (pre-refactor constants) pass → bit-stable refactor
- native↔WASM parity PASS, max delta unchanged (2.4e-7)
- chokepoint B compile: PAFSynth .text 122324→122692 (+0.30%, ±1% budget);
RAM +416B (eval scratch)
- fix: firmware-common.sh used bare 'python' (absent here) → ${PYTHON:-python3}
Part of one-core-engine-refactor P2. nisps_ml_create ABI untouched (P2.2 is
an operator stop-point).
2026-07-13 23:47:03 +02:00
|
|
|
} // namespace nisps::ml
|