refactor(ml): one runtime-configurable training default (S26)
The operator's call: "there should be one default learning rate and one
default max iterations and they should both be configurable at runtime."
There were SIX copies, not the four the audit described, and they did not
agree:
nisps/ml/mlp.hpp no-arg train() hardcoding 1.f / 1000u / 0.001f —
and firmware's ONLY training path calls exactly
this, so firmware had no runtime knob at all
wasm-iml.ts train() and trainAsync() TS default params (x2)
engine-api.ts learningRate ?? 1.0, with no maxIterations knob
vcv/src/iml.hpp 200 / 0.1 / 0.00001 — silently divergent
external_synth_midi.hpp its own kDefaultLearningRate/kDefaultMaxIterations
schemas/modes/*.json x9, identical, read by nobody at runtime
Now: schemas/ml_defaults.json is the single declaration (validated against a
sibling meta-schema, matching the midi_device.schema.json convention), codegen
emits it to C++ and TS in the same run, and MLPCore carries a TrainConfig whose
default member initialisers read the generated constant.
set_train_config()/nisps_ml_set_train_config() make it runtime-overridable on
every target; the explicit-argument train() overload is untouched. min_error
joins the tuple — it was duplicated identically and belongs with the other two.
The per-mode ml block loses default_learning_rate/default_max_iterations.
default_spread stays (genuinely wired on both targets) and input_channels stays
(codegen-time validated, real information for sound_analysis_midi).
VCV BEHAVIOUR CHANGE, deliberate: MEMLNaut.cpp constructs IML positionally and
relies on those defaults, so the module moves to 1000/1.0/0.001 — 5x the max
iterations, 10x the learning rate, and a 100x looser early-stop threshold. The
old values were never justified anywhere; they arrived with fbc68eb alongside
an unrelated module rewrite and no tuning rationale. Firmware and WASM have
shipped 1.0/1000 all along. It is now runtime-settable if this turns out worse.
The generated header lands in nisps/ml/generated/, not nisps/modes/generated/
where the rest of codegen output lives: training hyperparameters are an ML
fact, and nisps/ml sits below nisps/modes, so emitting them there would make
mlp.hpp include upward. The agent that built this flagged the directory-crossing
rather than hiding it; this is the fix. CI's generated-freshness gate learns the
new directory.
Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS (max delta 2.38e-7),
lint clean, manifold typecheck + 17 unit + 33 e2e (which exercise train() and
trainAsync() through a real browser).
This commit is contained in:
parent
b6be081cf0
commit
b16f26e6ab
49 changed files with 342 additions and 134 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -116,7 +116,7 @@ jobs:
|
||||||
bun run generate-midi-devices.ts
|
bun run generate-midi-devices.ts
|
||||||
bun run tests/golden_test.ts
|
bun run tests/golden_test.ts
|
||||||
cd ..
|
cd ..
|
||||||
GEN_DIRS="nisps/modes/generated nisps/midi/generated \
|
GEN_DIRS="nisps/modes/generated nisps/midi/generated nisps/ml/generated \
|
||||||
manifold/src/modes/generated manifold/src/midi-devices/generated"
|
manifold/src/modes/generated manifold/src/midi-devices/generated"
|
||||||
git add -N $GEN_DIRS
|
git add -N $GEN_DIRS
|
||||||
if ! git diff --exit-code -- $GEN_DIRS; then
|
if ! git diff --exit-code -- $GEN_DIRS; then
|
||||||
|
|
|
||||||
4
MAP.md
4
MAP.md
|
|
@ -6,7 +6,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
|
||||||
|
|
||||||
### `nisps/` — platform-agnostic C++20 library (the only ML/DSP/engine code)
|
### `nisps/` — platform-agnostic C++20 library (the only ML/DSP/engine code)
|
||||||
- `nisps/core/` — `perf.hpp` (hot-path/inlining attrs), `types.hpp`, `concepts.hpp` (`MLEngine`, `AudioEngine`, `Mode`), `ring_buffer.hpp` (SPSC lock-free cross-core channel, replaces pico/util/queue), `event_queue.hpp` (single-threaded in-engine event FIFO — deliberately NOT RingBuffer, which is an atomics-based cross-thread channel), `rng.hpp` (xoshiro256+ deterministic), `math.hpp` (fast_sigmoid, `Curve` enum + `apply_curve`).
|
- `nisps/core/` — `perf.hpp` (hot-path/inlining attrs), `types.hpp`, `concepts.hpp` (`MLEngine`, `AudioEngine`, `Mode`), `ring_buffer.hpp` (SPSC lock-free cross-core channel, replaces pico/util/queue), `event_queue.hpp` (single-threaded in-engine event FIFO — deliberately NOT RingBuffer, which is an atomics-based cross-thread channel), `rng.hpp` (xoshiro256+ deterministic), `math.hpp` (fast_sigmoid, `Curve` enum + `apply_curve`).
|
||||||
- `nisps/ml/` — the MLP core, written once against a storage policy (`mlp.hpp` `MLPCore<Storage>`): `storage.hpp` (`FixedStorage` — template-sized `std::array`, zero heap; `MLP<NIn,NH1,NH2,NH3,NOut>` alias preserves the classic compile-time surface) and `dynamic_storage.hpp` (`DynamicStorage` — runtime dims, single arena alloc at construction; `#error`s on RP2350 builds, sole lint heap-allowlist entry). Fixed↔dynamic bit-parity enforced by `tests/cpp/test_mlp_storage_parity.cpp`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (SGD + grad clipping), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `jolt.hpp` (`Jolt` — held continuous weight-morph over the flat weight buffer + post-release LR ramp; ported from upstream InterfaceRL), `ou_noise.hpp` (`OUNoise<N>` — Ornstein-Uhlenbeck exploration walk on the output vector; ported from upstream InterfaceRL), `feedback.hpp` (`FeedbackControllerCore<FbStorage>` — the "Down Action" state machine: Avoid (geometric push-away default / Diffuse legacy) / RandomiseOutputs / RandomiseMlp / ExploreAndPlace; storage-policied like the MLP, own deterministic RNG, exposed via `nisps_ml_feedback_*` C API), `replay.hpp` (`ReplayView` — reward-tagged memory: dedup/deepen, k-NN positive centroid with deterministic tie-break, proportional decay+eviction), `geo_push.hpp` (push-away target computation, upstream InterfaceRL @ 0a541cc), `warm_start.hpp` (overlapping-weights copy for reshape), `stats.hpp`. Jolt + OU are inert by default and wired into `ModeBase`, so every mode exposes `jolt_press/jolt_release`, `jolt_lr_scale`, and `set_explore_intensity`.
|
- `nisps/ml/` — the MLP core, written once against a storage policy (`mlp.hpp` `MLPCore<Storage>`): `storage.hpp` (`FixedStorage` — template-sized `std::array`, zero heap; `MLP<NIn,NH1,NH2,NH3,NOut>` alias preserves the classic compile-time surface) and `dynamic_storage.hpp` (`DynamicStorage` — runtime dims, single arena alloc at construction; `#error`s on RP2350 builds, sole lint heap-allowlist entry). Fixed↔dynamic bit-parity enforced by `tests/cpp/test_mlp_storage_parity.cpp`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (SGD + grad clipping), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `jolt.hpp` (`Jolt` — held continuous weight-morph over the flat weight buffer + post-release LR ramp; ported from upstream InterfaceRL), `ou_noise.hpp` (`OUNoise<N>` — Ornstein-Uhlenbeck exploration walk on the output vector; ported from upstream InterfaceRL), `feedback.hpp` (`FeedbackControllerCore<FbStorage>` — the "Down Action" state machine: Avoid (geometric push-away default / Diffuse legacy) / RandomiseOutputs / RandomiseMlp / ExploreAndPlace; storage-policied like the MLP, own deterministic RNG, exposed via `nisps_ml_feedback_*` C API), `replay.hpp` (`ReplayView` — reward-tagged memory: dedup/deepen, k-NN positive centroid with deterministic tie-break, proportional decay+eviction), `geo_push.hpp` (push-away target computation, upstream InterfaceRL @ 0a541cc), `warm_start.hpp` (overlapping-weights copy for reshape), `stats.hpp`. `generated/ml_defaults.hpp` is codegen output (do not edit): `nisps::ml::generated::kMlTrainDefaults`, the ONE learning-rate / max-iterations / min-error default shared by firmware, WASM and VCV (source `schemas/ml_defaults.json`); `MLPCore::set_train_config()` and `nisps_ml_set_train_config()` override it at runtime. It lives under `ml/` rather than `modes/generated/` because `nisps/ml` sits below `nisps/modes` — mlp.hpp must not include upward. Jolt + OU are inert by default and wired into `ModeBase`, so every mode exposes `jolt_press/jolt_release`, `jolt_lr_scale`, and `set_explore_intensity`.
|
||||||
- `nisps/pipeline/` — the control-rate input/output processing chains (P4): `input_chain.hpp` (`InputChain` — invert→deadzone→circular clamp→momentum-modulated zoom→centred power→EMA→momentum; caller-supplied dt, internal clock, fixed velocity ring, serialisable state) and `output_chain.hpp` (`OutputChain<NMax>` — curve→EMA→slew→freeze(+mask), capacity-templated). Behaviour contract = the retired manifold TS pipelines, pinned by `manifold/tests/fixtures/` and parity stage 7.
|
- `nisps/pipeline/` — the control-rate input/output processing chains (P4): `input_chain.hpp` (`InputChain` — invert→deadzone→circular clamp→momentum-modulated zoom→centred power→EMA→momentum; caller-supplied dt, internal clock, fixed velocity ring, serialisable state) and `output_chain.hpp` (`OutputChain<NMax>` — curve→EMA→slew→freeze(+mask), capacity-templated). Behaviour contract = the retired manifold TS pipelines, pinned by `manifold/tests/fixtures/` and parity stage 7.
|
||||||
- `nisps/dsp/` — `biquad.hpp`, `delay.hpp`, `reverb.hpp`, `filter.hpp`, `env.hpp`, `osc.hpp`, `pitch_shift.hpp`, `dc_blocker.hpp`, plus the sequencer primitives shared by the sequencer engines: `ratio_seq.hpp` and `seq_clock.hpp` (bar phasor + MIDI clock + bpm). Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.
|
- `nisps/dsp/` — `biquad.hpp`, `delay.hpp`, `reverb.hpp`, `filter.hpp`, `env.hpp`, `osc.hpp`, `pitch_shift.hpp`, `dc_blocker.hpp`, plus the sequencer primitives shared by the sequencer engines: `ratio_seq.hpp` and `seq_clock.hpp` (bar phasor + MIDI clock + bpm). Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl.
|
||||||
- `nisps/engines/` — eight audio engines, each satisfying `AudioEngine`: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp` (sequencer, NoOp audio), `elysiamorf.hpp` (sequencer, NoOp audio), `analysis.hpp` (input-side spectral features). Plus `base.hpp` (`NoOpEngine`, engine_id "thru").
|
- `nisps/engines/` — eight audio engines, each satisfying `AudioEngine`: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp` (sequencer, NoOp audio), `elysiamorf.hpp` (sequencer, NoOp audio), `analysis.hpp` (input-side spectral features). Plus `base.hpp` (`NoOpEngine`, engine_id "thru").
|
||||||
|
|
@ -141,7 +141,7 @@ includes; no `nisps-core`.
|
||||||
- **Parity check**: `bash scripts/parity-check.sh`.
|
- **Parity check**: `bash scripts/parity-check.sh`.
|
||||||
- **All tests**: `bash scripts/run-all-tests.sh`.
|
- **All tests**: `bash scripts/run-all-tests.sh`.
|
||||||
- **Playwright**: `cd manifold && node node_modules/.bin/playwright test` (non-snap node runner on the VPS — BUILD-PLAN gotcha; `bunx playwright test` works elsewhere).
|
- **Playwright**: `cd manifold && node node_modules/.bin/playwright test` (non-snap node runner on the VPS — BUILD-PLAN gotcha; `bunx playwright test` works elsewhere).
|
||||||
- **Codegen**: `cd codegen && bun run generate.ts` (regenerates both `nisps/modes/generated/` C++ and `manifold/src/modes/generated/` TS).
|
- **Codegen**: `cd codegen && bun run generate.ts` (regenerates `nisps/modes/generated/` + `nisps/ml/generated/` C++ and `manifold/src/modes/generated/` TS).
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@ interface ModeSchema {
|
||||||
hidden_layers: number[];
|
hidden_layers: number[];
|
||||||
output_size: number;
|
output_size: number;
|
||||||
default_spread: number;
|
default_spread: number;
|
||||||
default_learning_rate: number;
|
|
||||||
default_max_iterations: number;
|
|
||||||
};
|
};
|
||||||
params: Array<{
|
params: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -71,6 +69,24 @@ interface ModeSchema {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ONE global training-hyperparameter default (schemas/ml_defaults.json,
|
||||||
|
* validated against schemas/ml_defaults.schema.json) — NOT per-mode, unlike
|
||||||
|
* everything else in ModeSchema. See S26 (docs/specs/recon/
|
||||||
|
* simplification-audit-2026-07.md): learning_rate/max_iterations/min_error
|
||||||
|
* used to be duplicated per-mode (identically, unread at runtime) plus
|
||||||
|
* hardcoded separately in nisps/ml/mlp.hpp, manifold/src/engine/wasm-iml.ts,
|
||||||
|
* and vcv/src/iml.hpp. Now declared once here and consumed by
|
||||||
|
* `nisps::ml::MLPCore`'s `TrainConfig` default member initialisers.
|
||||||
|
*/
|
||||||
|
interface MlTrainDefaults {
|
||||||
|
$schema?: string;
|
||||||
|
_note?: string;
|
||||||
|
learning_rate: number;
|
||||||
|
max_iterations: number;
|
||||||
|
min_error: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Path resolution ------------------------------------------------------
|
// ----- Path resolution ------------------------------------------------------
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -78,7 +94,15 @@ const REPO_ROOT = resolve(__dirname, "..");
|
||||||
const SCHEMAS_DIR = join(REPO_ROOT, "schemas");
|
const SCHEMAS_DIR = join(REPO_ROOT, "schemas");
|
||||||
const MODES_DIR = join(SCHEMAS_DIR, "modes");
|
const MODES_DIR = join(SCHEMAS_DIR, "modes");
|
||||||
const META_SCHEMA_PATH = join(SCHEMAS_DIR, "schema.json");
|
const META_SCHEMA_PATH = join(SCHEMAS_DIR, "schema.json");
|
||||||
|
const ML_DEFAULTS_PATH = join(SCHEMAS_DIR, "ml_defaults.json");
|
||||||
|
const ML_DEFAULTS_SCHEMA_PATH = join(SCHEMAS_DIR, "ml_defaults.schema.json");
|
||||||
const CPP_OUT_DIR = join(REPO_ROOT, "nisps", "modes", "generated");
|
const CPP_OUT_DIR = join(REPO_ROOT, "nisps", "modes", "generated");
|
||||||
|
// The training defaults are an ML fact, not a mode fact, and `nisps/ml` sits
|
||||||
|
// BELOW `nisps/modes` in the layering MAP.md documents — emitting them into
|
||||||
|
// modes/generated/ would make mlp.hpp include upward. They get their own
|
||||||
|
// output dir on the C++ side. The TS side has no such layering to respect, so
|
||||||
|
// it keeps everything generated under one directory.
|
||||||
|
const CPP_ML_OUT_DIR = join(REPO_ROOT, "nisps", "ml", "generated");
|
||||||
const TS_OUT_DIR = join(REPO_ROOT, "manifold", "src", "modes", "generated");
|
const TS_OUT_DIR = join(REPO_ROOT, "manifold", "src", "modes", "generated");
|
||||||
|
|
||||||
// ----- Helpers --------------------------------------------------------------
|
// ----- Helpers --------------------------------------------------------------
|
||||||
|
|
@ -173,8 +197,6 @@ function emitSchemaTypesHpp(): string {
|
||||||
" std::size_t input_size;",
|
" std::size_t input_size;",
|
||||||
" std::size_t output_size;",
|
" std::size_t output_size;",
|
||||||
" float default_spread;",
|
" float default_spread;",
|
||||||
" float default_learning_rate;",
|
|
||||||
" std::size_t default_max_iterations;",
|
|
||||||
"};",
|
"};",
|
||||||
"",
|
"",
|
||||||
"enum class PrimaryInput : unsigned char {",
|
"enum class PrimaryInput : unsigned char {",
|
||||||
|
|
@ -209,8 +231,6 @@ function emitSchemaTypesHpp(): string {
|
||||||
" std::span<const std::size_t> hidden_layers;",
|
" std::span<const std::size_t> hidden_layers;",
|
||||||
" std::size_t output_size;",
|
" std::size_t output_size;",
|
||||||
" float default_spread;",
|
" float default_spread;",
|
||||||
" float default_learning_rate;",
|
|
||||||
" std::size_t default_max_iterations;",
|
|
||||||
" std::span<const ::nisps::modes::generated::Param> params;",
|
" std::span<const ::nisps::modes::generated::Param> params;",
|
||||||
" std::span<const std::string_view> voice_spaces;",
|
" std::span<const std::string_view> voice_spaces;",
|
||||||
" ::nisps::modes::generated::UIConfig ui;",
|
" ::nisps::modes::generated::UIConfig ui;",
|
||||||
|
|
@ -261,8 +281,6 @@ function emitSharedTsTypes(): string {
|
||||||
" readonly hidden_layers: readonly number[];",
|
" readonly hidden_layers: readonly number[];",
|
||||||
" readonly output_size: number;",
|
" readonly output_size: number;",
|
||||||
" readonly default_spread: number;",
|
" readonly default_spread: number;",
|
||||||
" readonly default_learning_rate: number;",
|
|
||||||
" readonly default_max_iterations: number;",
|
|
||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
"export interface UIConfig {",
|
"export interface UIConfig {",
|
||||||
|
|
@ -325,8 +343,6 @@ function emitModeHpp(schema: ModeSchema, sourceFile: string): string {
|
||||||
lines.push(` ${schema.ml.input_size}u,`);
|
lines.push(` ${schema.ml.input_size}u,`);
|
||||||
lines.push(` ${schema.ml.output_size}u,`);
|
lines.push(` ${schema.ml.output_size}u,`);
|
||||||
lines.push(` ${cppFloatLit(schema.ml.default_spread)},`);
|
lines.push(` ${cppFloatLit(schema.ml.default_spread)},`);
|
||||||
lines.push(` ${cppFloatLit(schema.ml.default_learning_rate)},`);
|
|
||||||
lines.push(` ${schema.ml.default_max_iterations}u,`);
|
|
||||||
lines.push("};");
|
lines.push("};");
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
|
|
@ -399,8 +415,6 @@ function emitModeHpp(schema: ModeSchema, sourceFile: string): string {
|
||||||
lines.push(` std::span<const std::size_t>(${constName}HiddenLayers),`);
|
lines.push(` std::span<const std::size_t>(${constName}HiddenLayers),`);
|
||||||
lines.push(` ${constName}MLConfig.output_size,`);
|
lines.push(` ${constName}MLConfig.output_size,`);
|
||||||
lines.push(` ${constName}MLConfig.default_spread,`);
|
lines.push(` ${constName}MLConfig.default_spread,`);
|
||||||
lines.push(` ${constName}MLConfig.default_learning_rate,`);
|
|
||||||
lines.push(` ${constName}MLConfig.default_max_iterations,`);
|
|
||||||
lines.push(` std::span<const Param>(${constName}Params),`);
|
lines.push(` std::span<const Param>(${constName}Params),`);
|
||||||
lines.push(` std::span<const std::string_view>(${constName}VoiceSpaces),`);
|
lines.push(` std::span<const std::string_view>(${constName}VoiceSpaces),`);
|
||||||
lines.push(` ${constName}UI,`);
|
lines.push(` ${constName}UI,`);
|
||||||
|
|
@ -452,8 +466,6 @@ function emitModeTs(schema: ModeSchema, sourceFile: string): string {
|
||||||
lines.push(" ],");
|
lines.push(" ],");
|
||||||
lines.push(` output_size: ${schema.ml.output_size},`);
|
lines.push(` output_size: ${schema.ml.output_size},`);
|
||||||
lines.push(` default_spread: ${schema.ml.default_spread},`);
|
lines.push(` default_spread: ${schema.ml.default_spread},`);
|
||||||
lines.push(` default_learning_rate: ${schema.ml.default_learning_rate},`);
|
|
||||||
lines.push(` default_max_iterations: ${schema.ml.default_max_iterations},`);
|
|
||||||
lines.push(" },");
|
lines.push(" },");
|
||||||
lines.push(" params: [");
|
lines.push(" params: [");
|
||||||
for (const p of schema.params) {
|
for (const p of schema.params) {
|
||||||
|
|
@ -519,6 +531,73 @@ function emitTsIndex(modeIds: string[]): string {
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Global ML-defaults emission (S26) ------------------------------------
|
||||||
|
// The ONE learning_rate/max_iterations/min_error default (schemas/ml_defaults.
|
||||||
|
// json), NOT per-mode — emitted once, alongside schema_types.hpp/types.ts,
|
||||||
|
// rather than once per mode file like everything else in this module.
|
||||||
|
|
||||||
|
const AUTOGEN_BANNER_ML_DEFAULTS = (lang: "C++" | "TS"): string =>
|
||||||
|
`// AUTOGENERATED (${lang}) — do not edit. Source: schemas/ml_defaults.json. ` +
|
||||||
|
"Run `bun run codegen/generate.ts` to regenerate.";
|
||||||
|
|
||||||
|
function emitMlDefaultsHpp(d: MlTrainDefaults): string {
|
||||||
|
return [
|
||||||
|
AUTOGEN_BANNER_ML_DEFAULTS("C++"),
|
||||||
|
"// The ONE global training-hyperparameter default, shared by every mode on",
|
||||||
|
"// every platform (firmware/WASM/VCV) — see docs/specs/recon/",
|
||||||
|
"// simplification-audit-2026-07.md S26. Consumed by",
|
||||||
|
"// nisps::ml::MLPCore::TrainConfig's default member initialisers",
|
||||||
|
"// (nisps/ml/mlp.hpp); nisps_ml_set_train_config() and",
|
||||||
|
"// nisps::ml::MLPCore::set_train_config() make it runtime-overridable.",
|
||||||
|
"#ifndef NISPS_ML_GENERATED_ML_DEFAULTS_HPP",
|
||||||
|
"#define NISPS_ML_GENERATED_ML_DEFAULTS_HPP",
|
||||||
|
"",
|
||||||
|
"#include <cstddef>",
|
||||||
|
"",
|
||||||
|
"namespace nisps::ml::generated {",
|
||||||
|
"",
|
||||||
|
"struct MlTrainDefaults {",
|
||||||
|
" float learning_rate;",
|
||||||
|
" std::size_t max_iterations;",
|
||||||
|
" float min_error;",
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
"inline constexpr MlTrainDefaults kMlTrainDefaults = {",
|
||||||
|
` ${cppFloatLit(d.learning_rate)},`,
|
||||||
|
` ${d.max_iterations}u,`,
|
||||||
|
` ${cppFloatLit(d.min_error)},`,
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
"} // namespace nisps::ml::generated",
|
||||||
|
"",
|
||||||
|
"#endif // NISPS_ML_GENERATED_ML_DEFAULTS_HPP",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitMlDefaultsTs(d: MlTrainDefaults): string {
|
||||||
|
return [
|
||||||
|
AUTOGEN_BANNER_ML_DEFAULTS("TS"),
|
||||||
|
"// The ONE global training-hyperparameter default, shared by every mode on",
|
||||||
|
"// every platform — see docs/specs/recon/simplification-audit-2026-07.md S26.",
|
||||||
|
"// Consumed by WasmIML's train()/trainAsync() default parameters and",
|
||||||
|
"// EngineApi's learningRate/maxIterations options (manifold/src/engine/).",
|
||||||
|
"",
|
||||||
|
"export interface MlTrainDefaults {",
|
||||||
|
" readonly learningRate: number;",
|
||||||
|
" readonly maxIterations: number;",
|
||||||
|
" readonly minError: number;",
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
"export const ML_TRAIN_DEFAULTS: MlTrainDefaults = {",
|
||||||
|
` learningRate: ${d.learning_rate},`,
|
||||||
|
` maxIterations: ${d.max_iterations},`,
|
||||||
|
` minError: ${d.min_error},`,
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Driver --------------------------------------------------------------
|
// ----- Driver --------------------------------------------------------------
|
||||||
|
|
||||||
function main(): number {
|
function main(): number {
|
||||||
|
|
@ -539,6 +618,29 @@ function main(): number {
|
||||||
});
|
});
|
||||||
const validate = ajv.compile<ModeSchema>(metaSchema);
|
const validate = ajv.compile<ModeSchema>(metaSchema);
|
||||||
|
|
||||||
|
// 1b. Load, compile, and validate the ONE global ML training default
|
||||||
|
// (schemas/ml_defaults.json against schemas/ml_defaults.schema.json — S26,
|
||||||
|
// NOT per-mode, so it lives outside the modeFiles loop below).
|
||||||
|
if (!existsSync(ML_DEFAULTS_SCHEMA_PATH)) {
|
||||||
|
console.error(`error: ml-defaults meta-schema not found at ${ML_DEFAULTS_SCHEMA_PATH}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!existsSync(ML_DEFAULTS_PATH)) {
|
||||||
|
console.error(`error: ml-defaults data not found at ${ML_DEFAULTS_PATH}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const mlDefaultsSchema = readJSON<AnySchemaObject>(ML_DEFAULTS_SCHEMA_PATH);
|
||||||
|
const validateMlDefaults = ajv.compile<MlTrainDefaults>(mlDefaultsSchema);
|
||||||
|
const mlDefaultsRaw = readJSON<unknown>(ML_DEFAULTS_PATH);
|
||||||
|
if (!validateMlDefaults(mlDefaultsRaw)) {
|
||||||
|
console.error(`error: ${ML_DEFAULTS_PATH}: schema validation failed:`);
|
||||||
|
for (const err of validateMlDefaults.errors ?? []) {
|
||||||
|
console.error(` ${err.instancePath || "<root>"} ${err.message}`);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const mlDefaults = mlDefaultsRaw as MlTrainDefaults;
|
||||||
|
|
||||||
// 2. Discover all mode schemas
|
// 2. Discover all mode schemas
|
||||||
if (!existsSync(MODES_DIR)) {
|
if (!existsSync(MODES_DIR)) {
|
||||||
console.error(`error: modes directory not found at ${MODES_DIR}`);
|
console.error(`error: modes directory not found at ${MODES_DIR}`);
|
||||||
|
|
@ -620,6 +722,8 @@ function main(): number {
|
||||||
// 4. Emit C++ outputs
|
// 4. Emit C++ outputs
|
||||||
ensureDir(CPP_OUT_DIR);
|
ensureDir(CPP_OUT_DIR);
|
||||||
writeFileSync(join(CPP_OUT_DIR, "schema_types.hpp"), emitSchemaTypesHpp());
|
writeFileSync(join(CPP_OUT_DIR, "schema_types.hpp"), emitSchemaTypesHpp());
|
||||||
|
ensureDir(CPP_ML_OUT_DIR);
|
||||||
|
writeFileSync(join(CPP_ML_OUT_DIR, "ml_defaults.hpp"), emitMlDefaultsHpp(mlDefaults));
|
||||||
for (const { source, schema } of schemas) {
|
for (const { source, schema } of schemas) {
|
||||||
const out = join(CPP_OUT_DIR, `${schema.mode_id}_schema.hpp`);
|
const out = join(CPP_OUT_DIR, `${schema.mode_id}_schema.hpp`);
|
||||||
writeFileSync(out, emitModeHpp(schema, source));
|
writeFileSync(out, emitModeHpp(schema, source));
|
||||||
|
|
@ -628,6 +732,7 @@ function main(): number {
|
||||||
// 5. Emit TS outputs
|
// 5. Emit TS outputs
|
||||||
ensureDir(TS_OUT_DIR);
|
ensureDir(TS_OUT_DIR);
|
||||||
writeFileSync(join(TS_OUT_DIR, "types.ts"), emitSharedTsTypes());
|
writeFileSync(join(TS_OUT_DIR, "types.ts"), emitSharedTsTypes());
|
||||||
|
writeFileSync(join(TS_OUT_DIR, "ml_defaults.ts"), emitMlDefaultsTs(mlDefaults));
|
||||||
for (const { source, schema } of schemas) {
|
for (const { source, schema } of schemas) {
|
||||||
const out = join(TS_OUT_DIR, `${schema.mode_id}_schema.ts`);
|
const out = join(TS_OUT_DIR, `${schema.mode_id}_schema.ts`);
|
||||||
writeFileSync(out, emitModeTs(schema, source));
|
writeFileSync(out, emitModeTs(schema, source));
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kPafSynthMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
33u,
|
33u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kPafSynthParamCount = 33u;
|
inline constexpr std::size_t kPafSynthParamCount = 33u;
|
||||||
|
|
@ -359,8 +357,6 @@ inline constexpr ::nisps::ParamSchema kPafSynthSchema = {
|
||||||
std::span<const std::size_t>(kPafSynthHiddenLayers),
|
std::span<const std::size_t>(kPafSynthHiddenLayers),
|
||||||
kPafSynthMLConfig.output_size,
|
kPafSynthMLConfig.output_size,
|
||||||
kPafSynthMLConfig.default_spread,
|
kPafSynthMLConfig.default_spread,
|
||||||
kPafSynthMLConfig.default_learning_rate,
|
|
||||||
kPafSynthMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kPafSynthParams),
|
std::span<const Param>(kPafSynthParams),
|
||||||
std::span<const std::string_view>(kPafSynthVoiceSpaces),
|
std::span<const std::string_view>(kPafSynthVoiceSpaces),
|
||||||
kPafSynthUI,
|
kPafSynthUI,
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,6 @@ export const PafSynthSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 33,
|
output_size: 33,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EngineHost } from './engine-host';
|
import { EngineHost } from './engine-host';
|
||||||
|
import { ML_TRAIN_DEFAULTS } from '../modes/generated/ml_defaults';
|
||||||
import type { InputConfig, OutputConfig } from './pipeline-types';
|
import type { InputConfig, OutputConfig } from './pipeline-types';
|
||||||
import { Spine, type BackendSend } from './spine';
|
import { Spine, type BackendSend } from './spine';
|
||||||
import type { EngineId, FeedbackMode, LayerStats } from './types';
|
import type { EngineId, FeedbackMode, LayerStats } from './types';
|
||||||
|
|
@ -106,6 +107,8 @@ export interface EngineApiOptions {
|
||||||
maxExamples?: number;
|
maxExamples?: number;
|
||||||
/** Default learning rate for thumbsUp/train. */
|
/** Default learning rate for thumbsUp/train. */
|
||||||
learningRate?: number;
|
learningRate?: number;
|
||||||
|
/** Default max training iterations for train/trainAsync. */
|
||||||
|
maxIterations?: number;
|
||||||
/** Default RL move speed / spread for thumbsDown. */
|
/** Default RL move speed / spread for thumbsDown. */
|
||||||
noiseCap?: number;
|
noiseCap?: number;
|
||||||
spread?: number;
|
spread?: number;
|
||||||
|
|
@ -123,6 +126,7 @@ export class EngineApi {
|
||||||
private host: EngineHost;
|
private host: EngineHost;
|
||||||
|
|
||||||
private learningRate: number;
|
private learningRate: number;
|
||||||
|
private maxIterations: number;
|
||||||
private noiseCap: number;
|
private noiseCap: number;
|
||||||
private spread_: number;
|
private spread_: number;
|
||||||
|
|
||||||
|
|
@ -134,9 +138,15 @@ export class EngineApi {
|
||||||
this.iml = iml;
|
this.iml = iml;
|
||||||
this.spine = spine;
|
this.spine = spine;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.learningRate = opts.learningRate ?? 1.0;
|
this.learningRate = opts.learningRate ?? ML_TRAIN_DEFAULTS.learningRate;
|
||||||
|
this.maxIterations = opts.maxIterations ?? ML_TRAIN_DEFAULTS.maxIterations;
|
||||||
this.noiseCap = opts.noiseCap ?? 0.3;
|
this.noiseCap = opts.noiseCap ?? 0.3;
|
||||||
this.spread_ = opts.spread ?? 0.6;
|
this.spread_ = opts.spread ?? 0.6;
|
||||||
|
// Persist the configured default on the underlying MLP too (S26) — makes
|
||||||
|
// the WASM engine's OWN training config match EngineApi's knobs, the same
|
||||||
|
// real runtime-configurability firmware/VCV get for free from
|
||||||
|
// MLPCore::TrainConfig's default member initialisers.
|
||||||
|
this.iml.setTrainConfig(this.learningRate, this.maxIterations, ML_TRAIN_DEFAULTS.minError);
|
||||||
if (opts.debugClockDt !== undefined) this.spine.setFixedDt(opts.debugClockDt);
|
if (opts.debugClockDt !== undefined) this.spine.setFixedDt(opts.debugClockDt);
|
||||||
|
|
||||||
// Wire the spine's backend.send to push routed params into the worklet.
|
// Wire the spine's backend.send to push routed params into the worklet.
|
||||||
|
|
@ -291,11 +301,11 @@ export class EngineApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
train(): number {
|
train(): number {
|
||||||
return this.iml.train(this.learningRate);
|
return this.iml.train(this.learningRate, this.maxIterations);
|
||||||
}
|
}
|
||||||
|
|
||||||
trainAsync(): Promise<number> {
|
trainAsync(): Promise<number> {
|
||||||
return this.iml.trainAsync(this.learningRate);
|
return this.iml.trainAsync(this.learningRate, this.maxIterations);
|
||||||
}
|
}
|
||||||
|
|
||||||
randomise(spread = this.spread_): void {
|
randomise(spread = this.spread_): void {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ export interface NispsModule {
|
||||||
// ML training.
|
// ML training.
|
||||||
_nisps_ml_add_example(ml: number, features_ptr: number, labels_ptr: number): void;
|
_nisps_ml_add_example(ml: number, features_ptr: number, labels_ptr: number): void;
|
||||||
_nisps_ml_train(ml: number, lr: number, max_iter: number, min_err: number, sample_weights_ptr: number): number;
|
_nisps_ml_train(ml: number, lr: number, max_iter: number, min_err: number, sample_weights_ptr: number): number;
|
||||||
|
// Persist a new training-hyperparameter default on the handle (S26) — mirrors
|
||||||
|
// nisps::ml::MLPCore::set_train_config. Does not train.
|
||||||
|
_nisps_ml_set_train_config(ml: number, lr: number, max_iter: number, min_err: number): void;
|
||||||
_nisps_ml_eval_loss(ml: number): number;
|
_nisps_ml_eval_loss(ml: number): number;
|
||||||
|
|
||||||
// ML examples.
|
// ML examples.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dataset } from './dataset';
|
import { Dataset } from './dataset';
|
||||||
|
import { ML_TRAIN_DEFAULTS } from '../modes/generated/ml_defaults';
|
||||||
import {
|
import {
|
||||||
anchorModeToInt,
|
anchorModeToInt,
|
||||||
momentumModeToInt,
|
momentumModeToInt,
|
||||||
|
|
@ -180,6 +181,13 @@ export class WasmIML {
|
||||||
dataset!: Dataset;
|
dataset!: Dataset;
|
||||||
private readonly sink: EngineSink;
|
private readonly sink: EngineSink;
|
||||||
private lastLoss_: number | null = null;
|
private lastLoss_: number | null = null;
|
||||||
|
// JS-side mirror of the training-hyperparameter default, seeded from the
|
||||||
|
// ONE generated constant (S26, docs/specs/recon/simplification-audit-2026-07
|
||||||
|
// .md) rather than hardcoded literals. `setTrainConfig` updates this AND the
|
||||||
|
// WASM handle's own copy (nisps_ml_set_train_config) so train()/trainAsync()
|
||||||
|
// fall back to a genuinely runtime-configurable default, not just a JS
|
||||||
|
// literal.
|
||||||
|
private trainConfig = { ...ML_TRAIN_DEFAULTS };
|
||||||
private trainer: WasmTrainer | null = null;
|
private trainer: WasmTrainer | null = null;
|
||||||
private storageKey: string;
|
private storageKey: string;
|
||||||
private saveTimer: number | null = null;
|
private saveTimer: number | null = null;
|
||||||
|
|
@ -620,7 +628,22 @@ export class WasmIML {
|
||||||
this.module._nisps_ml_add_example(this.mlHandle, this.featuresBuf.ptr, this.labelsBuf.ptr);
|
this.module._nisps_ml_add_example(this.mlHandle, this.featuresBuf.ptr, this.labelsBuf.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
train(lr = 1.0, maxIter = 1000, minErr = 0.001, sampleWeights?: Float32Array): number {
|
/** Persist a new training-hyperparameter default (S26): updates the JS-side
|
||||||
|
* mirror used as the train()/trainAsync() fallback AND the WASM handle's
|
||||||
|
* own copy via the C API, so the underlying MLP is genuinely
|
||||||
|
* runtime-configurable rather than just remembering a number to pass on
|
||||||
|
* each call. */
|
||||||
|
setTrainConfig(lr: number, maxIter: number, minErr: number): void {
|
||||||
|
this.trainConfig = { learningRate: lr, maxIterations: maxIter, minError: minErr };
|
||||||
|
this.module._nisps_ml_set_train_config(this.mlHandle, lr, maxIter, minErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
train(
|
||||||
|
lr = this.trainConfig.learningRate,
|
||||||
|
maxIter = this.trainConfig.maxIterations,
|
||||||
|
minErr = this.trainConfig.minError,
|
||||||
|
sampleWeights?: Float32Array,
|
||||||
|
): number {
|
||||||
if (this.dataset.isEmpty()) {
|
if (this.dataset.isEmpty()) {
|
||||||
this.lastLoss_ = 0;
|
this.lastLoss_ = 0;
|
||||||
this.sink.setState({ lastLoss: 0 });
|
this.sink.setState({ lastLoss: 0 });
|
||||||
|
|
@ -653,7 +676,12 @@ export class WasmIML {
|
||||||
return loss;
|
return loss;
|
||||||
}
|
}
|
||||||
|
|
||||||
async trainAsync(lr = 1.0, maxIter = 1000, minErr = 0.001, sampleWeights?: Float32Array): Promise<number> {
|
async trainAsync(
|
||||||
|
lr = this.trainConfig.learningRate,
|
||||||
|
maxIter = this.trainConfig.maxIterations,
|
||||||
|
minErr = this.trainConfig.minError,
|
||||||
|
sampleWeights?: Float32Array,
|
||||||
|
): Promise<number> {
|
||||||
if (this.dataset.isEmpty()) {
|
if (this.dataset.isEmpty()) {
|
||||||
this.lastLoss_ = 0;
|
this.lastLoss_ = 0;
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,6 @@ export const BreakorSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 56,
|
output_size: 56,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,6 @@ export const ChannelStripSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 24,
|
output_size: 24,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,6 @@ export const ElysiamorfSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 40,
|
output_size: 40,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,6 @@ export const MemlceliumSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 56,
|
output_size: 56,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
17
manifold/src/modes/generated/ml_defaults.ts
Normal file
17
manifold/src/modes/generated/ml_defaults.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// AUTOGENERATED (TS) — do not edit. Source: schemas/ml_defaults.json. Run `bun run codegen/generate.ts` to regenerate.
|
||||||
|
// The ONE global training-hyperparameter default, shared by every mode on
|
||||||
|
// every platform — see docs/specs/recon/simplification-audit-2026-07.md S26.
|
||||||
|
// Consumed by WasmIML's train()/trainAsync() default parameters and
|
||||||
|
// EngineApi's learningRate/maxIterations options (manifold/src/engine/).
|
||||||
|
|
||||||
|
export interface MlTrainDefaults {
|
||||||
|
readonly learningRate: number;
|
||||||
|
readonly maxIterations: number;
|
||||||
|
readonly minError: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ML_TRAIN_DEFAULTS: MlTrainDefaults = {
|
||||||
|
learningRate: 1,
|
||||||
|
maxIterations: 1000,
|
||||||
|
minError: 0.001,
|
||||||
|
};
|
||||||
|
|
@ -55,8 +55,6 @@ export const PafSynthSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 33,
|
output_size: 33,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,6 @@ export const SlpWorkshopSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 56,
|
output_size: 56,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@ export const SoundAnalysisMidiSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 8,
|
output_size: 8,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,6 @@ export interface MLConfig {
|
||||||
readonly hidden_layers: readonly number[];
|
readonly hidden_layers: readonly number[];
|
||||||
readonly output_size: number;
|
readonly output_size: number;
|
||||||
readonly default_spread: number;
|
readonly default_spread: number;
|
||||||
readonly default_learning_rate: number;
|
|
||||||
readonly default_max_iterations: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UIConfig {
|
export interface UIConfig {
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,6 @@ export const VerbFxSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 47,
|
output_size: 47,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,6 @@ export const XiasriSchema: ModeSchema = {
|
||||||
],
|
],
|
||||||
output_size: 24,
|
output_size: 24,
|
||||||
default_spread: 0.6,
|
default_spread: 0.6,
|
||||||
default_learning_rate: 1,
|
|
||||||
default_max_iterations: 1000,
|
|
||||||
},
|
},
|
||||||
params: [
|
params: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
29
nisps/ml/generated/ml_defaults.hpp
Normal file
29
nisps/ml/generated/ml_defaults.hpp
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// AUTOGENERATED (C++) — do not edit. Source: schemas/ml_defaults.json. Run `bun run codegen/generate.ts` to regenerate.
|
||||||
|
// The ONE global training-hyperparameter default, shared by every mode on
|
||||||
|
// every platform (firmware/WASM/VCV) — see docs/specs/recon/
|
||||||
|
// simplification-audit-2026-07.md S26. Consumed by
|
||||||
|
// nisps::ml::MLPCore::TrainConfig's default member initialisers
|
||||||
|
// (nisps/ml/mlp.hpp); nisps_ml_set_train_config() and
|
||||||
|
// nisps::ml::MLPCore::set_train_config() make it runtime-overridable.
|
||||||
|
#ifndef NISPS_ML_GENERATED_ML_DEFAULTS_HPP
|
||||||
|
#define NISPS_ML_GENERATED_ML_DEFAULTS_HPP
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace nisps::ml::generated {
|
||||||
|
|
||||||
|
struct MlTrainDefaults {
|
||||||
|
float learning_rate;
|
||||||
|
std::size_t max_iterations;
|
||||||
|
float min_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr MlTrainDefaults kMlTrainDefaults = {
|
||||||
|
1.0f,
|
||||||
|
1000u,
|
||||||
|
0.001f,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nisps::ml::generated
|
||||||
|
|
||||||
|
#endif // NISPS_ML_GENERATED_ML_DEFAULTS_HPP
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
#include "../core/concepts.hpp"
|
#include "../core/concepts.hpp"
|
||||||
#include "../core/perf.hpp"
|
#include "../core/perf.hpp"
|
||||||
#include "../core/rng.hpp"
|
#include "../core/rng.hpp"
|
||||||
|
#include "generated/ml_defaults.hpp"
|
||||||
#include "activations.hpp"
|
#include "activations.hpp"
|
||||||
#include "init.hpp"
|
#include "init.hpp"
|
||||||
#include "loss.hpp"
|
#include "loss.hpp"
|
||||||
|
|
@ -58,6 +59,30 @@
|
||||||
|
|
||||||
namespace nisps::ml {
|
namespace nisps::ml {
|
||||||
|
|
||||||
|
// Per-instance training-hyperparameter config (S26, docs/specs/recon/
|
||||||
|
// simplification-audit-2026-07.md): the ONE learning_rate/max_iterations/
|
||||||
|
// min_error default used to be duplicated identically across all nine
|
||||||
|
// schemas/modes/*.json (unread at runtime), hardcoded again in this file's
|
||||||
|
// no-arg train() overload, again in manifold's wasm-iml.ts TS default
|
||||||
|
// parameters, and a FOURTH time (diverging: 0.1/200/0.00001) in vcv/src/
|
||||||
|
// iml.hpp. Default member initialisers below pull the single generated
|
||||||
|
// constant (schemas/ml_defaults.json -> nisps/ml/generated/ml_defaults.hpp)
|
||||||
|
// so every MLPCore instance — firmware, WASM handle, VCV adapter — starts
|
||||||
|
// pre-configured identically; `set_train_config` makes it runtime-overridable,
|
||||||
|
// same as the codebase-wide decision requires.
|
||||||
|
//
|
||||||
|
// Note on layering: ml_defaults.hpp is generated into nisps/ml/generated/, NOT
|
||||||
|
// alongside schema_types.hpp in nisps/modes/generated/ where the rest of the
|
||||||
|
// codegen output lives. Training hyperparameters are an ML fact, not a mode
|
||||||
|
// fact, and nisps/ml sits below nisps/modes — emitting them there would make
|
||||||
|
// this file include upward. The TS side has no equivalent layering to respect
|
||||||
|
// and keeps all generated output in one directory.
|
||||||
|
struct TrainConfig {
|
||||||
|
float learning_rate = ::nisps::ml::generated::kMlTrainDefaults.learning_rate;
|
||||||
|
std::size_t max_iterations = ::nisps::ml::generated::kMlTrainDefaults.max_iterations;
|
||||||
|
float min_error = ::nisps::ml::generated::kMlTrainDefaults.min_error;
|
||||||
|
};
|
||||||
|
|
||||||
// Activation of layer L in the fixed 4-layer topology.
|
// Activation of layer L in the fixed 4-layer topology.
|
||||||
template <std::size_t L>
|
template <std::size_t L>
|
||||||
inline constexpr Activation kLayerActivation =
|
inline constexpr Activation kLayerActivation =
|
||||||
|
|
@ -139,11 +164,24 @@ class MLPCore : public Storage {
|
||||||
for (std::size_t i = 0; i < n_out; ++i) dsl[l_off + i] = labels[i];
|
for (std::size_t i = 0; i < n_out; ++i) dsl[l_off + i] = labels[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Concept-required no-arg overload.
|
// Concept-required no-arg overload. Reads the runtime-configurable
|
||||||
|
// `train_config_` (default-initialised from the single generated default;
|
||||||
|
// see `TrainConfig` above) rather than hardcoding numbers here.
|
||||||
float train() noexcept {
|
float train() noexcept {
|
||||||
return train(1.f, 1000u, 0.001f, std::span<const float>{});
|
return train(train_config_.learning_rate, train_config_.max_iterations,
|
||||||
|
train_config_.min_error, std::span<const float>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runtime knob for the no-arg train() overload (S26). Does not affect the
|
||||||
|
// explicit-argument train() below, which stays the always-available
|
||||||
|
// explicit path.
|
||||||
|
void set_train_config(float lr, std::size_t max_iter, float min_err) noexcept {
|
||||||
|
train_config_.learning_rate = lr;
|
||||||
|
train_config_.max_iterations = max_iter;
|
||||||
|
train_config_.min_error = min_err;
|
||||||
|
}
|
||||||
|
const TrainConfig& train_config() const noexcept { return train_config_; }
|
||||||
|
|
||||||
// Full SGD training. `sample_weights`, if non-empty, must size to the
|
// Full SGD training. `sample_weights`, if non-empty, must size to the
|
||||||
// current example count and sum to 1.0 (caller's responsibility — we
|
// current example count and sum to 1.0 (caller's responsibility — we
|
||||||
// do NOT renormalize).
|
// do NOT renormalize).
|
||||||
|
|
@ -556,6 +594,7 @@ class MLPCore : public Storage {
|
||||||
std::size_t dataset_count_ = 0u;
|
std::size_t dataset_count_ = 0u;
|
||||||
std::size_t dataset_head_ = 0u;
|
std::size_t dataset_head_ = 0u;
|
||||||
std::size_t loss_history_count_ = 0u;
|
std::size_t loss_history_count_ = 0u;
|
||||||
|
TrainConfig train_config_{};
|
||||||
|
|
||||||
Rng rng_;
|
Rng rng_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,12 @@ namespace nisps::modes {
|
||||||
// instantiation (see the file-header comment for why these are hand-named
|
// instantiation (see the file-header comment for why these are hand-named
|
||||||
// rather than codegen'd): 4 joystick inputs -> [10,14,18] hidden -> NOut
|
// rather than codegen'd): 4 joystick inputs -> [10,14,18] hidden -> NOut
|
||||||
// (device-CC-count-driven) outputs, matching every other 4-joystick-input
|
// (device-CC-count-driven) outputs, matching every other 4-joystick-input
|
||||||
// mode's schema defaults (default_spread 0.6, default_learning_rate 1.0,
|
// mode's schema default_spread (0.6). The training hyperparameter defaults
|
||||||
// default_max_iterations 1000).
|
// (learning_rate/max_iterations/min_error) are no longer part of
|
||||||
|
// `ParamSchema` at all (S26) — they live once, globally, in
|
||||||
|
// `nisps::ml::generated::kMlTrainDefaults` (nisps/ml/generated/
|
||||||
|
// ml_defaults.hpp) and are wired via `nisps::ml::MLPCore::TrainConfig`'s
|
||||||
|
// default member initialisers instead.
|
||||||
namespace ext_synth_defaults {
|
namespace ext_synth_defaults {
|
||||||
inline constexpr std::size_t kInputSize = 4u;
|
inline constexpr std::size_t kInputSize = 4u;
|
||||||
inline constexpr std::array<std::string_view, 4> kInputChannels{
|
inline constexpr std::array<std::string_view, 4> kInputChannels{
|
||||||
|
|
@ -66,8 +70,6 @@ inline constexpr std::array<std::string_view, 4> kInputChannels{
|
||||||
std::string_view{"joy_z"}, std::string_view{"joy_w"}};
|
std::string_view{"joy_z"}, std::string_view{"joy_w"}};
|
||||||
inline constexpr std::array<std::size_t, 3> kHiddenLayers{10u, 14u, 18u};
|
inline constexpr std::array<std::size_t, 3> kHiddenLayers{10u, 14u, 18u};
|
||||||
inline constexpr float kDefaultSpread = 0.6f;
|
inline constexpr float kDefaultSpread = 0.6f;
|
||||||
inline constexpr float kDefaultLearningRate = 1.0f;
|
|
||||||
inline constexpr std::size_t kDefaultMaxIterations = 1000u;
|
|
||||||
} // namespace ext_synth_defaults
|
} // namespace ext_synth_defaults
|
||||||
|
|
||||||
// The net-shape alias every instantiation uses (mirrors S6/S25's per-mode
|
// The net-shape alias every instantiation uses (mirrors S6/S25's per-mode
|
||||||
|
|
@ -185,8 +187,6 @@ class ExternalSynthMIDIMode : public ModeBase<
|
||||||
std::span<const std::size_t>(ext_synth_defaults::kHiddenLayers),
|
std::span<const std::size_t>(ext_synth_defaults::kHiddenLayers),
|
||||||
NOut,
|
NOut,
|
||||||
ext_synth_defaults::kDefaultSpread,
|
ext_synth_defaults::kDefaultSpread,
|
||||||
ext_synth_defaults::kDefaultLearningRate,
|
|
||||||
ext_synth_defaults::kDefaultMaxIterations,
|
|
||||||
std::span<const generated::Param>(kNoParams),
|
std::span<const generated::Param>(kNoParams),
|
||||||
std::span<const std::string_view>(kNoVoiceSpaces),
|
std::span<const std::string_view>(kNoVoiceSpaces),
|
||||||
kUI,
|
kUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kBreakorMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
56u,
|
56u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kBreakorParamCount = 56u;
|
inline constexpr std::size_t kBreakorParamCount = 56u;
|
||||||
|
|
@ -558,8 +556,6 @@ inline constexpr ::nisps::ParamSchema kBreakorSchema = {
|
||||||
std::span<const std::size_t>(kBreakorHiddenLayers),
|
std::span<const std::size_t>(kBreakorHiddenLayers),
|
||||||
kBreakorMLConfig.output_size,
|
kBreakorMLConfig.output_size,
|
||||||
kBreakorMLConfig.default_spread,
|
kBreakorMLConfig.default_spread,
|
||||||
kBreakorMLConfig.default_learning_rate,
|
|
||||||
kBreakorMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kBreakorParams),
|
std::span<const Param>(kBreakorParams),
|
||||||
std::span<const std::string_view>(kBreakorVoiceSpaces),
|
std::span<const std::string_view>(kBreakorVoiceSpaces),
|
||||||
kBreakorUI,
|
kBreakorUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kChannelStripMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
24u,
|
24u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kChannelStripParamCount = 24u;
|
inline constexpr std::size_t kChannelStripParamCount = 24u;
|
||||||
|
|
@ -277,8 +275,6 @@ inline constexpr ::nisps::ParamSchema kChannelStripSchema = {
|
||||||
std::span<const std::size_t>(kChannelStripHiddenLayers),
|
std::span<const std::size_t>(kChannelStripHiddenLayers),
|
||||||
kChannelStripMLConfig.output_size,
|
kChannelStripMLConfig.output_size,
|
||||||
kChannelStripMLConfig.default_spread,
|
kChannelStripMLConfig.default_spread,
|
||||||
kChannelStripMLConfig.default_learning_rate,
|
|
||||||
kChannelStripMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kChannelStripParams),
|
std::span<const Param>(kChannelStripParams),
|
||||||
std::span<const std::string_view>(kChannelStripVoiceSpaces),
|
std::span<const std::string_view>(kChannelStripVoiceSpaces),
|
||||||
kChannelStripUI,
|
kChannelStripUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kElysiamorfMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
40u,
|
40u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kElysiamorfParamCount = 40u;
|
inline constexpr std::size_t kElysiamorfParamCount = 40u;
|
||||||
|
|
@ -414,8 +412,6 @@ inline constexpr ::nisps::ParamSchema kElysiamorfSchema = {
|
||||||
std::span<const std::size_t>(kElysiamorfHiddenLayers),
|
std::span<const std::size_t>(kElysiamorfHiddenLayers),
|
||||||
kElysiamorfMLConfig.output_size,
|
kElysiamorfMLConfig.output_size,
|
||||||
kElysiamorfMLConfig.default_spread,
|
kElysiamorfMLConfig.default_spread,
|
||||||
kElysiamorfMLConfig.default_learning_rate,
|
|
||||||
kElysiamorfMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kElysiamorfParams),
|
std::span<const Param>(kElysiamorfParams),
|
||||||
std::span<const std::string_view>(kElysiamorfVoiceSpaces),
|
std::span<const std::string_view>(kElysiamorfVoiceSpaces),
|
||||||
kElysiamorfUI,
|
kElysiamorfUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kMemlceliumMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
56u,
|
56u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kMemlceliumParamCount = 56u;
|
inline constexpr std::size_t kMemlceliumParamCount = 56u;
|
||||||
|
|
@ -560,8 +558,6 @@ inline constexpr ::nisps::ParamSchema kMemlceliumSchema = {
|
||||||
std::span<const std::size_t>(kMemlceliumHiddenLayers),
|
std::span<const std::size_t>(kMemlceliumHiddenLayers),
|
||||||
kMemlceliumMLConfig.output_size,
|
kMemlceliumMLConfig.output_size,
|
||||||
kMemlceliumMLConfig.default_spread,
|
kMemlceliumMLConfig.default_spread,
|
||||||
kMemlceliumMLConfig.default_learning_rate,
|
|
||||||
kMemlceliumMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kMemlceliumParams),
|
std::span<const Param>(kMemlceliumParams),
|
||||||
std::span<const std::string_view>(kMemlceliumVoiceSpaces),
|
std::span<const std::string_view>(kMemlceliumVoiceSpaces),
|
||||||
kMemlceliumUI,
|
kMemlceliumUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kPafSynthMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
33u,
|
33u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kPafSynthParamCount = 33u;
|
inline constexpr std::size_t kPafSynthParamCount = 33u;
|
||||||
|
|
@ -359,8 +357,6 @@ inline constexpr ::nisps::ParamSchema kPafSynthSchema = {
|
||||||
std::span<const std::size_t>(kPafSynthHiddenLayers),
|
std::span<const std::size_t>(kPafSynthHiddenLayers),
|
||||||
kPafSynthMLConfig.output_size,
|
kPafSynthMLConfig.output_size,
|
||||||
kPafSynthMLConfig.default_spread,
|
kPafSynthMLConfig.default_spread,
|
||||||
kPafSynthMLConfig.default_learning_rate,
|
|
||||||
kPafSynthMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kPafSynthParams),
|
std::span<const Param>(kPafSynthParams),
|
||||||
std::span<const std::string_view>(kPafSynthVoiceSpaces),
|
std::span<const std::string_view>(kPafSynthVoiceSpaces),
|
||||||
kPafSynthUI,
|
kPafSynthUI,
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,6 @@ struct MLConfig {
|
||||||
std::size_t input_size;
|
std::size_t input_size;
|
||||||
std::size_t output_size;
|
std::size_t output_size;
|
||||||
float default_spread;
|
float default_spread;
|
||||||
float default_learning_rate;
|
|
||||||
std::size_t default_max_iterations;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class PrimaryInput : unsigned char {
|
enum class PrimaryInput : unsigned char {
|
||||||
|
|
@ -73,8 +71,6 @@ struct ParamSchema {
|
||||||
std::span<const std::size_t> hidden_layers;
|
std::span<const std::size_t> hidden_layers;
|
||||||
std::size_t output_size;
|
std::size_t output_size;
|
||||||
float default_spread;
|
float default_spread;
|
||||||
float default_learning_rate;
|
|
||||||
std::size_t default_max_iterations;
|
|
||||||
std::span<const ::nisps::modes::generated::Param> params;
|
std::span<const ::nisps::modes::generated::Param> params;
|
||||||
std::span<const std::string_view> voice_spaces;
|
std::span<const std::string_view> voice_spaces;
|
||||||
::nisps::modes::generated::UIConfig ui;
|
::nisps::modes::generated::UIConfig ui;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kSlpWorkshopMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
56u,
|
56u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kSlpWorkshopParamCount = 56u;
|
inline constexpr std::size_t kSlpWorkshopParamCount = 56u;
|
||||||
|
|
@ -560,8 +558,6 @@ inline constexpr ::nisps::ParamSchema kSlpWorkshopSchema = {
|
||||||
std::span<const std::size_t>(kSlpWorkshopHiddenLayers),
|
std::span<const std::size_t>(kSlpWorkshopHiddenLayers),
|
||||||
kSlpWorkshopMLConfig.output_size,
|
kSlpWorkshopMLConfig.output_size,
|
||||||
kSlpWorkshopMLConfig.default_spread,
|
kSlpWorkshopMLConfig.default_spread,
|
||||||
kSlpWorkshopMLConfig.default_learning_rate,
|
|
||||||
kSlpWorkshopMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kSlpWorkshopParams),
|
std::span<const Param>(kSlpWorkshopParams),
|
||||||
std::span<const std::string_view>(kSlpWorkshopVoiceSpaces),
|
std::span<const std::string_view>(kSlpWorkshopVoiceSpaces),
|
||||||
kSlpWorkshopUI,
|
kSlpWorkshopUI,
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@ inline constexpr MLConfig kSoundAnalysisMidiMLConfig = {
|
||||||
10u,
|
10u,
|
||||||
8u,
|
8u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kSoundAnalysisMidiParamCount = 8u;
|
inline constexpr std::size_t kSoundAnalysisMidiParamCount = 8u;
|
||||||
|
|
@ -132,8 +130,6 @@ inline constexpr ::nisps::ParamSchema kSoundAnalysisMidiSchema = {
|
||||||
std::span<const std::size_t>(kSoundAnalysisMidiHiddenLayers),
|
std::span<const std::size_t>(kSoundAnalysisMidiHiddenLayers),
|
||||||
kSoundAnalysisMidiMLConfig.output_size,
|
kSoundAnalysisMidiMLConfig.output_size,
|
||||||
kSoundAnalysisMidiMLConfig.default_spread,
|
kSoundAnalysisMidiMLConfig.default_spread,
|
||||||
kSoundAnalysisMidiMLConfig.default_learning_rate,
|
|
||||||
kSoundAnalysisMidiMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kSoundAnalysisMidiParams),
|
std::span<const Param>(kSoundAnalysisMidiParams),
|
||||||
std::span<const std::string_view>(kSoundAnalysisMidiVoiceSpaces),
|
std::span<const std::string_view>(kSoundAnalysisMidiVoiceSpaces),
|
||||||
kSoundAnalysisMidiUI,
|
kSoundAnalysisMidiUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kVerbFxMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
47u,
|
47u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kVerbFxParamCount = 47u;
|
inline constexpr std::size_t kVerbFxParamCount = 47u;
|
||||||
|
|
@ -490,8 +488,6 @@ inline constexpr ::nisps::ParamSchema kVerbFxSchema = {
|
||||||
std::span<const std::size_t>(kVerbFxHiddenLayers),
|
std::span<const std::size_t>(kVerbFxHiddenLayers),
|
||||||
kVerbFxMLConfig.output_size,
|
kVerbFxMLConfig.output_size,
|
||||||
kVerbFxMLConfig.default_spread,
|
kVerbFxMLConfig.default_spread,
|
||||||
kVerbFxMLConfig.default_learning_rate,
|
|
||||||
kVerbFxMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kVerbFxParams),
|
std::span<const Param>(kVerbFxParams),
|
||||||
std::span<const std::string_view>(kVerbFxVoiceSpaces),
|
std::span<const std::string_view>(kVerbFxVoiceSpaces),
|
||||||
kVerbFxUI,
|
kVerbFxUI,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ inline constexpr MLConfig kXiasriMLConfig = {
|
||||||
4u,
|
4u,
|
||||||
24u,
|
24u,
|
||||||
0.6f,
|
0.6f,
|
||||||
1.0f,
|
|
||||||
1000u,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr std::size_t kXiasriParamCount = 24u;
|
inline constexpr std::size_t kXiasriParamCount = 24u;
|
||||||
|
|
@ -272,8 +270,6 @@ inline constexpr ::nisps::ParamSchema kXiasriSchema = {
|
||||||
std::span<const std::size_t>(kXiasriHiddenLayers),
|
std::span<const std::size_t>(kXiasriHiddenLayers),
|
||||||
kXiasriMLConfig.output_size,
|
kXiasriMLConfig.output_size,
|
||||||
kXiasriMLConfig.default_spread,
|
kXiasriMLConfig.default_spread,
|
||||||
kXiasriMLConfig.default_learning_rate,
|
|
||||||
kXiasriMLConfig.default_max_iterations,
|
|
||||||
std::span<const Param>(kXiasriParams),
|
std::span<const Param>(kXiasriParams),
|
||||||
std::span<const std::string_view>(kXiasriVoiceSpaces),
|
std::span<const std::string_view>(kXiasriVoiceSpaces),
|
||||||
kXiasriUI,
|
kXiasriUI,
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,20 @@ float nisps_ml_train(void* ml, float lr, int max_iter, float min_err,
|
||||||
return h->mlp.train(lr, static_cast<std::size_t>(max_iter), min_err, weights);
|
return h->mlp.train(lr, static_cast<std::size_t>(max_iter), min_err, weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist a new training-hyperparameter default on the handle itself (S26):
|
||||||
|
// mirrors nisps::ml::MLPCore::set_train_config so the browser MLP is
|
||||||
|
// genuinely runtime-configurable, not just JS remembering a number to pass on
|
||||||
|
// each nisps_ml_train() call. Does not train; only reconfigures the no-arg
|
||||||
|
// train() fallback (unused WASM-side today, but keeps the handle's own state
|
||||||
|
// consistent with firmware/VCV, which carry the same knob).
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void nisps_ml_set_train_config(void* ml, float lr, int max_iter, float min_err) {
|
||||||
|
if (!ml) return;
|
||||||
|
auto* h = static_cast<MLHandle*>(ml);
|
||||||
|
if (max_iter <= 0) max_iter = 1;
|
||||||
|
h->mlp.set_train_config(lr, static_cast<std::size_t>(max_iter), min_err);
|
||||||
|
}
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
EMSCRIPTEN_KEEPALIVE
|
||||||
float nisps_ml_eval_loss(void* ml) {
|
float nisps_ml_eval_loss(void* ml) {
|
||||||
if (!ml) return 0.f;
|
if (!ml) return 0.f;
|
||||||
|
|
|
||||||
7
schemas/ml_defaults.json
Normal file
7
schemas/ml_defaults.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "ml_defaults.schema.json",
|
||||||
|
"_note": "The ONE default learning rate / max iterations / min-error for training, shared by every mode on every platform (audit S26, docs/specs/recon/simplification-audit-2026-07.md). Values preserve the numbers previously hardcoded in nisps/ml/mlp.hpp's no-arg train() and manifold/src/engine/wasm-iml.ts's TS default parameters, so wiring this up is numerically a no-op for firmware and WASM. It is NOT a no-op for vcv/src/iml.hpp, whose own private defaults (200 / 0.1 / 0.00001) diverged and have been deleted in the same change (see vcv/src/iml.hpp's IML constructor comment) — a real behaviour change to the VCV module.",
|
||||||
|
"learning_rate": 1.0,
|
||||||
|
"max_iterations": 1000,
|
||||||
|
"min_error": 0.001
|
||||||
|
}
|
||||||
31
schemas/ml_defaults.schema.json
Normal file
31
schemas/ml_defaults.schema.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://memlnaut/schemas/ml_defaults.schema.json",
|
||||||
|
"title": "MEMLNaut ML Training Defaults",
|
||||||
|
"description": "Single, global declaration of the MLP training-hyperparameter defaults (learning rate, max iterations, min-error/convergence threshold) — ONE value per field, shared by every mode on every platform (firmware, WASM/manifold, VCV). This is only the DEFAULT: codegen/generate.ts emits it to nisps/modes/generated/ml_defaults.hpp (C++) and manifold/src/modes/generated/ml_defaults.ts (TS), and nisps::ml::MLPCore::set_train_config()/train_config() (nisps/ml/mlp.hpp) makes it overridable at runtime on every target. Per-mode ml.default_spread stays separate — it is genuinely per-mode-tuned, unlike these.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["learning_rate", "max_iterations", "min_error"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"$schema": { "type": "string" },
|
||||||
|
"_note": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Free-text provenance note from the schema author."
|
||||||
|
},
|
||||||
|
"learning_rate": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0.0,
|
||||||
|
"description": "SGD learning rate the no-arg train() falls back to."
|
||||||
|
},
|
||||||
|
"max_iterations": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Epoch cap for the no-arg train() fallback."
|
||||||
|
},
|
||||||
|
"min_error": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"description": "Early-stop threshold: training stops once epoch loss drops below this value."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 14, 18],
|
"hidden_layers": [10, 14, 18],
|
||||||
"output_size": 56,
|
"output_size": 56,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "kick_ratio0", "label": "Kick Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "kick" },
|
{ "name": "kick_ratio0", "label": "Kick Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "kick" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 10, 14],
|
"hidden_layers": [10, 10, 14],
|
||||||
"output_size": 24,
|
"output_size": 24,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "pre_gain", "label": "Pre Gain", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "square", "group": "gain" },
|
{ "name": "pre_gain", "label": "Pre Gain", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "square", "group": "gain" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 14, 18],
|
"hidden_layers": [10, 14, 18],
|
||||||
"output_size": 40,
|
"output_size": 40,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "fm0_carrier", "label": "FM0 Carrier", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "fm0" },
|
{ "name": "fm0_carrier", "label": "FM0 Carrier", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "fm0" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 14, 18],
|
"hidden_layers": [10, 14, 18],
|
||||||
"output_size": 56,
|
"output_size": 56,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "seq0_ratio0", "label": "Seq0 Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
{ "name": "seq0_ratio0", "label": "Seq0 Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 10, 14],
|
"hidden_layers": [10, 10, 14],
|
||||||
"output_size": 33,
|
"output_size": 33,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "p00", "label": "Param 00", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "general" },
|
{ "name": "p00", "label": "Param 00", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "general" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 14, 18],
|
"hidden_layers": [10, 14, 18],
|
||||||
"output_size": 56,
|
"output_size": 56,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "seq0_ratio0", "label": "Seq0 Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
{ "name": "seq0_ratio0", "label": "Seq0 Ratio 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "sequencer" },
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
"input_size": 10,
|
"input_size": 10,
|
||||||
"hidden_layers": [10, 10, 14],
|
"hidden_layers": [10, 10, 14],
|
||||||
"output_size": 8,
|
"output_size": 8,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "midi_cc0", "label": "MIDI CC 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "midi" },
|
{ "name": "midi_cc0", "label": "MIDI CC 0", "min": 0.0, "max": 1.0, "default": 0.5, "curve": "linear", "group": "midi" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 14, 18],
|
"hidden_layers": [10, 14, 18],
|
||||||
"output_size": 47,
|
"output_size": 47,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "fb_delay_xfade", "label": "Bank/Delay XFade", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "routing" },
|
{ "name": "fb_delay_xfade", "label": "Bank/Delay XFade", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "routing" },
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"input_size": 4,
|
"input_size": 4,
|
||||||
"hidden_layers": [10, 10, 14],
|
"hidden_layers": [10, 10, 14],
|
||||||
"output_size": 24,
|
"output_size": 24,
|
||||||
"default_spread": 0.6,
|
"default_spread": 0.6
|
||||||
"default_learning_rate": 1.0,
|
|
||||||
"default_max_iterations": 1000
|
|
||||||
},
|
},
|
||||||
"params": [
|
"params": [
|
||||||
{ "name": "dl1_mix", "label": "Delay 1 Mix", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "delays" },
|
{ "name": "dl1_mix", "label": "Delay 1 Mix", "min": 0.0, "max": 1.0, "default": 0.0, "curve": "linear", "group": "delays" },
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,7 @@
|
||||||
"input_size",
|
"input_size",
|
||||||
"hidden_layers",
|
"hidden_layers",
|
||||||
"output_size",
|
"output_size",
|
||||||
"default_spread",
|
"default_spread"
|
||||||
"default_learning_rate",
|
|
||||||
"default_max_iterations"
|
|
||||||
],
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -58,9 +56,7 @@
|
||||||
"minItems": 1
|
"minItems": 1
|
||||||
},
|
},
|
||||||
"output_size": { "type": "integer", "minimum": 1 },
|
"output_size": { "type": "integer", "minimum": 1 },
|
||||||
"default_spread": { "type": "number", "minimum": 0.0, "maximum": 1.0 },
|
"default_spread": { "type": "number", "minimum": 0.0, "maximum": 1.0 }
|
||||||
"default_learning_rate": { "type": "number", "exclusiveMinimum": 0.0 },
|
|
||||||
"default_max_iterations": { "type": "integer", "minimum": 1 }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ EXPORTED_FUNCS='[
|
||||||
"_malloc","_free",
|
"_malloc","_free",
|
||||||
"_nisps_ml_create","_nisps_ml_destroy","_nisps_ml_reshape",
|
"_nisps_ml_create","_nisps_ml_destroy","_nisps_ml_reshape",
|
||||||
"_nisps_ml_set_input","_nisps_ml_process","_nisps_ml_outputs","_nisps_ml_infer_batch",
|
"_nisps_ml_set_input","_nisps_ml_process","_nisps_ml_outputs","_nisps_ml_infer_batch",
|
||||||
"_nisps_ml_add_example","_nisps_ml_train","_nisps_ml_eval_loss",
|
"_nisps_ml_add_example","_nisps_ml_train","_nisps_ml_set_train_config","_nisps_ml_eval_loss",
|
||||||
"_nisps_ml_clear_examples",
|
"_nisps_ml_clear_examples",
|
||||||
"_nisps_ml_weight_count","_nisps_ml_get_weights","_nisps_ml_set_weights",
|
"_nisps_ml_weight_count","_nisps_ml_get_weights","_nisps_ml_set_weights",
|
||||||
"_nisps_ml_draw_weights",
|
"_nisps_ml_draw_weights",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
#include "../../nisps/core/rng.hpp"
|
#include "../../nisps/core/rng.hpp"
|
||||||
#include "../../nisps/ml/dynamic_storage.hpp"
|
#include "../../nisps/ml/dynamic_storage.hpp"
|
||||||
#include "../../nisps/ml/mlp.hpp"
|
#include "../../nisps/ml/mlp.hpp"
|
||||||
|
#include "../../nisps/ml/generated/ml_defaults.hpp"
|
||||||
|
|
||||||
namespace nisps {
|
namespace nisps {
|
||||||
|
|
||||||
|
|
@ -77,11 +78,23 @@ class IML {
|
||||||
// are unchanged. `hidden` MUST carry exactly three sizes (the core topology
|
// are unchanged. `hidden` MUST carry exactly three sizes (the core topology
|
||||||
// is fixed at three hidden layers); fewer are padded from the default,
|
// is fixed at three hidden layers); fewer are padded from the default,
|
||||||
// extras ignored.
|
// extras ignored.
|
||||||
|
// BEHAVIOUR CHANGE (S26, docs/specs/recon/simplification-audit-2026-07.md):
|
||||||
|
// these three defaults used to be private to this adapter (200 / 0.1 /
|
||||||
|
// 0.00001) and disagreed with the firmware/WASM default (1000 / 1.0 /
|
||||||
|
// 0.001) that every other target already used. They now come from the
|
||||||
|
// ONE shared generated constant (schemas/ml_defaults.json ->
|
||||||
|
// nisps::ml::generated::kMlTrainDefaults) instead of a private copy.
|
||||||
|
// `MEMLNaut.cpp` constructs `IML` with only 3 positional args, so this is
|
||||||
|
// a REAL runtime behaviour change for the module: 5x more max iterations,
|
||||||
|
// 10x the learning rate, and a 100x looser (larger) early-stop threshold.
|
||||||
explicit IML(std::size_t n_inputs, std::size_t n_outputs,
|
explicit IML(std::size_t n_inputs, std::size_t n_outputs,
|
||||||
std::vector<std::size_t> hidden = {16u, 24u, 16u},
|
std::vector<std::size_t> hidden = {16u, 24u, 16u},
|
||||||
std::size_t max_iterations = 200u,
|
std::size_t max_iterations =
|
||||||
Float learning_rate = static_cast<Float>(0.1),
|
::nisps::ml::generated::kMlTrainDefaults.max_iterations,
|
||||||
Float convergence_threshold = static_cast<Float>(0.00001),
|
Float learning_rate = static_cast<Float>(
|
||||||
|
::nisps::ml::generated::kMlTrainDefaults.learning_rate),
|
||||||
|
Float convergence_threshold = static_cast<Float>(
|
||||||
|
::nisps::ml::generated::kMlTrainDefaults.min_error),
|
||||||
std::uint64_t seed = 0xC0FFEEu)
|
std::uint64_t seed = 0xC0FFEEu)
|
||||||
: n_inputs_(n_inputs),
|
: n_inputs_(n_inputs),
|
||||||
n_outputs_(n_outputs),
|
n_outputs_(n_outputs),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue