Repository navigation
Does this change a forecast for an existing config?
No. Every addition in 0.1.3 is opt-in and default-off. A config that does not set the new key behaves exactly as it did on 0.1.2 β verified by measurement, not assumption: the training loss was computed with and without the new per-task accumulator on identical input, and the total/regression/classification values matched to the bit.
(This question leads the notes because eight views-models models install this package through a range pin β views-hydranet~=0.1.1 β so they pick up a new version without choosing it. See #399.)
Why 0.1.3 and not 0.2.0
A new config field would normally argue for a minor bump. It is a patch deliberately: the pod runner pins >=0.1.1,<0.2.0, so a 0.2.0 would not be collected and would require an edit to production infrastructure in views-models. 0.1.3 is picked up with no runner change.
Added β body_nll_denominator
Selects what the zero-truncated negative-binomial body loss divides its per-cell losses by.
| value | meaning |
|---|---|
"positives" |
default. The count of cells with a positive target β the historical reduction, unchanged. |
"all_cells" |
Every supervised cell β the denominator the joint hurdle NLL actually has. |
Why it exists. Setting output_distribution: 'truncated_nb' silently moved this denominator from all cells to positive cells. At a 1-in-222 positive rate that rescaled one task-loss term 65Γ, and no config-identity check could see it, because arm_identity_check.py compares config values. Measured consequence on the shared trunk: the body's gradient-norm ratio against the gate went from 0.31 to 10.5 and cos(βbody, βgate) flipped from +0.72 to β0.20, with 75% of training windows in conflict.
"all_cells" also replaces a random denominator with a fixed one. The per-step positive count is often 0 or 1 for a rare target (median 0.88 positives per window for non-state violence), so E[Ξ£/N_pos] β E[Ξ£]/E[N_pos] β the "positives" gradient is biased and high-variance on rare targets.
Rejected where it would be inert, rather than accepted and ignored:
- requires
output_distribution: 'truncated_nb'β every other family already reduces over all cells - requires
body_supervision: 'all'β under'active'the supervision mask is a positive mask, so the denominator collapses back and the flag becomes bit-identical to"positives" - the rule is enforced once and invoked from two places, so a caller that bypasses the config schema (a raw dict driver) hits the same check
Added β per-task loss instrumentation
When trajectory_log_path is set, the trajectory CSV now carries one loss/<task> column per task (body and gate, per target) alongside the existing pooled loss_reg / loss_cls.
Previously the engine collapsed the balancer's task vector to two sums and nothing else, so a per-target loss question had to be reconstructed from saved weights after the fact. That is exactly how the 65Γ above was found β weeks late.
Fixed β the trajectory log can come home from a pod run
Its parent directory is now created. A pod's deliver directory only contains artifact/, parquet/ and draws/, so an absolute path is the only way this file survives the run β and without the mkdir it raised FileNotFoundError after the data pull and the model build.
Not included
- The balancer's learning rate never reaches its parameter group.
_LRScheduler.step()zips against a length-1get_lr(), so an unfrozen balancer trains at a constant0.001while the network decays. Pre-existing and latent β everyviews-modelsconfig pinsfreeze_multitask_balancer: Trueβ and excluded deliberately rather than changing training dynamics inside a correctness release. - A CHANGELOG (#399).
How it was reviewed
/code-review medium β /review-diff β an independent guard audit in a clean context (36 mutations across 7 categories; 10 survivors, each addressed or recorded as an equivalent mutant) β two further mutation rounds that each found another defect in the fixes.
That process caught, in order: the instrumentation shipping completely inert (a hand-built return dict dropped the key, so the CSV never gained a column); a guard that compared a list to itself; a validation rule stated twice unevenly so one caller skipped half of it; three stale interface contracts; and four separate guards that compared a value to itself or to the constant that produced it.
Final state: 2247 tests passing, guards mutation-verified, rehearsed on TestPyPI as 0.1.3.dev9 and installed back before this release was cut.
Full diff: v0.1.2...v0.1.3