Repository navigation
Conversation
CompareLossesOnCluster() opened with
if (pi_core_id() != 0)
return;
but it is dispatched through pi_cluster_send_task_to_cl() *without* a
fork, so it executes exactly once, on the cluster controller core. On
GAP9 the CC reports pi_core_id() == 8 (0..7 are the workers), so the
guard fired unconditionally. The comparison loop never executed,
*err_count kept its initial 0, and every training test printed
"Errors: 0 out of N" regardless of the computed losses.
Instrumented run (ResNet-8, L2 single-buffer, --l1 116000) before the
fix: the function is entered once, pi_core_id() == 8, the sentinel put
in a->n is never overwritten, no "[loss i]" line is emitted, and the
cluster-side copy of the computed losses stays 0x00000000. The test
reported "Errors: 0 out of 4" while the actual losses were
2.302585, 1.435176e+28, 1.435176e+28, 1.435055e+28
(reference 1.884626, 2.120300, 2.083486, 2.225912)
i.e. the first step is still at ln(10) and the run then overflows.
After the fix the same run reports "Errors: 4 out of 4" and fails.
Drop the guard on both harnesses. Every other *Wrapper entry point in
these files is already unguarded, which is the correct pattern for a
non-forked cluster task. Siracusa carried the same line; it is harmless
there (its cluster master is core 0) but is removed for consistency.
This makes previously green GAP9 training jobs go red. That is the
point: they were green because nothing was being checked. Re-running the
suite with CI's own markers, serially, gives 11 PASS / 15 FAIL across 26
configurations. The failures are not uniform and point at two distinct
bugs to chase separately:
- ResNet-8 and MobileNetV1 pass L3 single-buffer and L3 double-buffer
but fail 4/4 as soon as PromoteTensorsToL2 is enabled.
- The CCT family fails L3 (single, double, recompute) but CCT-QLoRA
passes in L2 and CCT-2 passes promote single-buffer, so the problem
is in L3 tiling rather than in the operators.
DS-CNN L2 and ResNet-8 L2 each fail 4/4 and look like a third,
independent issue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
generateTrainingTestFiles decided whether to emit the initial weight
arrays from the platform name alone:
_emit_init_weights = "GAP9" not in _platform_name
The intent (see the comment it carries) was that GAP9 already loads every
*L3-resident* weight from its generated N.hex file at boot, so baking the
same values into the binary a second time is redundant. That holds for
--defaultMemLevel L3. It is false for --defaultMemLevel L2: there the
weights live in L2, no hex file is emitted for them, and suppressing the
bake also emits TRAINING_SKIP_INITWEIGHT_COPY, so the harness skips its
copy too. All three supply paths are off at once and every trainable
weight stays zero.
A network with zero weights emits all-equal logits, so the cross-entropy
loss is pinned at exactly ln(num_classes). That is what the GAP9 on-chip
configurations were computing:
DS-CNN (12 classes) ln(12) = 2.484907 measured 2.484907
ResNet-8 (10 classes) ln(10) = 2.302585 measured 2.302585
both bit-exact, at every --l1 budget, while the same models pass in L3
and on Siracusa (whose testinputs.h embeds 621 KB of weight arrays).
Key the decision on where the weights actually live, per weight:
- a weight that is L3-resident keeps the hex path and gets a NULL slot
in testInitWeights[]; the harness skips NULL entries.
- a weight that is not L3-resident is staged through L3 as a hex file
and copied into its L2 buffer by the existing LoadTestDataL3() +
l3_aware_copy path.
Staging rather than baking matters: ResNet-8 on-chip needs 1,350,236 B of
arena and its weights are 312 KB, so a .data copy does not fit in GAP9's
1.5 MB L2_shared at all. The same reasoning applies to the baked test
images, so those follow the weights to L3 whenever the weights go there.
Also: .weightmem_sram is defined only by the Siracusa linker script, so
the section attribute is now optional and GAP9 uses the default section.
Siracusa's code generation is unchanged.
Measured on GAP9, tiled, single-buffer (4 mini-batches):
L2 SimpleMLP PASS static L2 68,704 B
L2 DS-CNN PASS -> was 4/4 errors, static L2 179,264 B
L2 ResNet-8 on-chip PASS -> was 4/4 errors, static L2 172,016 B
L3 Autoencoder PASS unchanged, bit-identical losses
L3 ResNet-8 PASS unchanged, bit-identical losses
L3 MobileNetV1 PASS unchanged, bit-identical losses
ResNet-8 on-chip now fits with all four mini-batches: its static L2 drops
from 521,536 B (weights baked) to 172,016 B, leaving the 1,350,236 B
arena 50 KB of headroom where it was previously 2,492 B short.
Two known failures are left untouched and are not caused by this change:
CCT-2 in L3 (forward is bit-exact at step 0, the weights are destroyed by
the first optimizer step) and CCT-QLoRA in L2, whose numerical error only
became visible now that its LoRA weights are actually loaded -- it used
to "pass" because a zeroed LoRA adapter contributes nothing and LoRA-B is
zero-initialised anyway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
Six GAP9 training models forced
"num_data_inputs": 1
The flag exists for one documented reason (see the note above Siracusa's
TRAINING_MODEL_OVERRIDES): "required when inputs.npz has only one
mini-batch (no mb1_arr_* entries) so the runner can't auto-detect". That
condition holds for none of the six -- every one of their inputs.npz
carries mb1, mb2 and mb3, so auto-detection works and returns the right
answer on its own.
The value is wrong as well. These graphs take two inputs that change per
mini-batch, e.g. CCT:
input [1, 3, 32, 32] FLOAT
labels [1] INT64
Declaring one makes TRAINING_NUM_DATA_INPUTS 1, and the harness indexes
the weights as DeeployNetwork_inputs[TRAINING_NUM_DATA_INPUTS + wi], so
the whole weight array shifts by one slot in the optimizer's copy-in and
copy-out. Step 0 is unaffected (the forward reads the weights Deeploy
loaded from hex), which is why the first loss stayed bit-exact while
every later one collapsed towards ln(num_classes).
Reproduced as a single-variable A/B on CCT-2, L3 single-buffer, l1=122000:
auto-detected (2) Errors: 0 out of 4 diffs <= 1e-4
--num-data-inputs 1 Errors: 3 out of 4 2.301697 / 2.302683 / 2.302585
the failing values being bit-identical to CI's. Siracusa never sets the
override -- its CCT entry is {} -- which is why the same models have been
passing there all along.
GAP9 L3 single-buffer goes from 3 passed / 6 failed to 7 passed / 2 failed:
CCT-2, SleepConViT, TSDR and MCUNet all recover. CCT_LoRA_R1 and
CCT_QLORA_FT still fail 4/4; removing the override does not make them
worse, and their remaining error is a separate issue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
Formatting only, no behaviour change. Two lines reflowed to yapf 0.43.0's output (the version pinned in .pre-commit-config.yaml) and `_weight_keys` removed -- it was superseded by `_weight_is_l3` in the commit it belongs to and left behind unused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
The field is a plain sum over the weight slots of DeeployNetwork_inputs[]:
the total size of the trainable weight tensors. It says nothing about
where those tensors live -- the same model reports the same number in an
on-chip-L2 and an L3 configuration -- so "sram" invited exactly the wrong
reading, that it measured on-chip footprint.
What it is good for is a parameter-count cross-check. It caught a real
mistake during this session: a re-measurement started on a branch without
the MLperf-compliant fixtures reported 10,928 B for DS-CNN instead of
92,720 B, which is 2,732 parameters instead of 23,180 -- the old
non-compliant model. Worth keeping, worth naming honestly.
Renamed in the two harnesses that emit it and the three parsers that read
it, and documented at both ends. The BENCH line is now
BENCH train_cycles=N opt_cycles=N trainable_bytes=N
Also drops the WEIGHTDUMP debug probe that was committed by accident in
"drop the bogus num_data_inputs=1 overrides"; it was inert (nothing
defines WEIGHTDUMP) but had no business shipping.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
Restoring the loss comparison (the guard `pi_core_id() != 0` never holds
on GAP9, whose cluster controller reports core id 8) made three
pre-existing failures visible. None of them ever passed; they only ever
printed "Errors: 0" because nothing was being compared. Recording them
is not accepting a regression.
They stay in the run -- compiled, simulated, numerically compared -- and
strict=True means an unexpected PASS fails the run, so an entry cannot
quietly rot here: CI will say when to delete it.
CCT_QLORA_FT (L2 + L3) and CCT_LoRA_R1 (L3)
The fixtures are wrong, not the deployment. The two ship
byte-identical inputs.npz and outputs.npz (md5 2d0875bc../a01d0bba..)
although their graphs differ: 387 nodes with no quantisation against
389 nodes with 14 Dequant. The quantised model is checked against
references generated from the float one. Measured error is 0.12-0.50;
the 5e-2 tolerance already in the config was an earlier attempt to
absorb exactly this. Widening it further would be the same mistake, so
the fix is to regenerate the references from each graph.
ResNet-8 with PromoteTensorsToL2 (singlebuffer + doublebuffer)
Promotion shifts the FP32 accumulation order. The step-0 forward alone
is off by 8.7e-4 against a 1e-3 tolerance where plain L3 is bit-exact,
and steps 1-3 drift to 0.005-0.025. Reproduced 3/3 with the same
3-of-4 count, so it is deterministic rather than flaky. MobileNetV1
and CCT-2 take the same promote path and stay within 1e-4, which makes
this look specific rather than inherent, but it is off the critical
path: ResNet-8's fastest verified configuration is on-chip L2 at
116.2 ms, faster than promote and numerically clean.
Verified on the L2 single-buffer group: 5 passed, 1 xfailed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
pi_l1_malloc returns NULL when the cluster-L1 allocator cannot satisfy the request. The SDK prints "Allocation failed for allocator 2" and the program carries on: the generated code computes every tile address from a NULL base, so the failure surfaces much later as an unrelated Invalid access at a garbage address -- or not at all, if the wild reads land on mapped memory and the run merely produces wrong numbers. That is the same shape of problem as the dead loss comparison this branch fixes: something is broken, nothing says so, and the evidence turns up far from the cause. Both GAP9 L1 allocation templates now abort at the point of failure and print the requested size, so the log names what happened. The budget that triggered it -- ResNet-8 on-chip at --l1 122000 against 118,672 B of free cluster L1 -- is corrected in the branch that raised it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
…ule buys
_memoryMinimisingScheduler has been the default for every training
deployment for a while, and the exported order it replaces had no switch,
so there was no way to measure what it is worth. This adds one.
--identitySchedule deploys in _mockScheduler's order -- the graph as the
exporter emitted it, which for a training graph is the whole forward and
then the whole backward. That order keeps every saved activation live
across the entire backward even where the gradient node consuming it runs
early, which is exactly what the list schedule exists to avoid.
Measured peak, identity -> mem_min: ResNet-8 1434.0 -> 1313.8 KB (-8.4%),
MobileNetV1 3594.9 -> 3351.9 (-6.8%), CCT-2 2535.1 -> 2471.6 (-2.5%),
Autoencoder 66.3 -> 65.0 (-2.0%), DS-CNN 64.7 -> 64.6 (-0.2%). All of it is
activation; persistent does not move. Errors: 0 out of 4 throughout, as it
must be -- ordering changes when tensors are live, never what is computed.
ResNet-8 on-chip L2 does not fit at all in the exported order ("Memory
allocator failed at memory level L2 with capacity of 1397280 bytes") and
fits with mem_min, so for that deployment the list schedule is not an
optimisation but a precondition.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
Formatting only. The pinned yapf (0.43.0, per .pre-commit-config.yaml) reflows the conditional in the scheduler selection and the two new add_argument calls; nothing else changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN
The Siracusa tiled training jobs (notably l3-doublebuffer) exceed the 6h runner limit and get cancelled; keep only the inference promote regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013w8xXNTjEDT261WWj3QnJC
AllocateTemplate.py: take devel's version, which already carries this branch's NULL check on the L1 arena allocation (#75). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013w8xXNTjEDT261WWj3QnJC
Every commit of this branch reached devel through the squash of #74 (97ae840), so the merge takes devel's side of the two conflicts (conftest.py: the ResNet-8 promote xfails were removed once the promoted tile offsets were fixed; deeployTrainingRunner_tiled_gap9.py: the --promoteToL2FetchBytes option). The branch now carries no change on top of devel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015eN7LrHjeZ7uqDDeQ5yzE9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #71 and #72, which were the same work split into a stack. Each of them was red on its own — not because anything was broken in it, but because #71 restores a check that #72 then satisfies, so the middle of the stack could never be green. Merged here as one change, and CI is green.
The dead check
CompareLossesOnCluster()opened withbut it is dispatched through
pi_cluster_send_task_to_cl()without a fork, so it runs exactly once, on the cluster controller. On GAP9 that core reportspi_core_id() == 8(0..7 are the workers), so the guard fired every time. The comparison loop never executed,*err_countkept its initial0, and every GAP9 training test printedErrors: 0 out of Nno matter what the network computed.Instrumented, ResNet-8 / L2 single-buffer:
pi_core_id()a->nread4[loss i]linesErrors: 0 out of 4→ PASSEDErrors: 4 out of 4→ FAILEDThe losses that run actually produced:
2.302585, 1.435176e+28, 1.435176e+28, 1.435055e+28against references1.884626, 2.120300, 2.083486, 2.225912.2.302585isln(10)— the first step was still at the random initialisation, and it overflowed from there.Siracusa carries the same line but its cluster master is core 0, so its check has been live all along. That is why the same models pass there, and it is what made the rest of this diagnosable.
What the check then found
Initial weights were never loaded with
--defaultMemLevel L2The bake was keyed on the platform name:
The intent was that GAP9 hex-loads every L3-resident weight at boot, so baking them again is redundant. True in L3. In L2 the weights live in L2, no hex file is emitted, and suppressing the bake also emits
TRAINING_SKIP_INITWEIGHT_COPY, so the harness skips its copy too. All three supply paths off at once; every trainable weight stays zero.Zero weights give all-equal logits, so the loss sits at exactly
ln(num_classes):Bit-exact, at every
--l1. The decision is now per weight and keyed on where the weight actually lives: L3-resident ones keep the hex path and get aNULLslot the harness skips; the rest are staged through L3 as hex and copied in by the existingLoadTestDataL3()+l3_aware_copy. Staging rather than baking matters — ResNet-8 on-chip needs 1,350,236 B of arena and its weights are 312 KB, which does not fit in 1.5 MB ofL2_sharedat all.Six models forced a wrong
num_data_inputs"num_data_inputs": 1on CCT, CCT_LoRA_R1, CCT_QLORA_FT, SleepConViT, TSDR and MCUNet. The flag exists for one documented case — "required wheninputs.npzhas only one mini-batch" — which holds for none of them; every one carriesmb1,mb2,mb3.The value is wrong too. These graphs take two inputs that change per mini-batch (CCT:
input [1,3,32,32]andlabels [1]). Declaring one shifts the harness's weight indexing by a slot in the optimizer's copy-in and copy-out. Step 0 is unaffected, which is why the first loss stayed bit-exact while the later ones collapsed.Single-variable A/B on CCT-2, L3 single-buffer,
l1=122000:the failing values bit-identical to CI's. Siracusa never sets the override, which is again why it was unaffected.
pi_l1_mallocfailures were silentThe SDK prints
Allocation failed for allocator 2and continues. The generated code then computes every tile address from a NULL base, so the failure appears much later as an unrelatedInvalid access— or not at all, if the wild reads land on mapped memory and the run just produces wrong numbers. Both GAP9 L1 allocation templates now abort at the point of failure with the requested size.Known failures, recorded rather than hidden
Three tests still fail. They never passed — they printed
Errors: 0because nothing was compared — soxfail(strict=True)records a pre-existing state rather than accepting a regression. They still compile, simulate and compare;strict=Truemeans an unexpected PASS fails the run, so no entry can rot here. All five entries sit in one table inconftest.pywith the evidence attached.CCT_QLORA_FT (L2 + L3) and CCT_LoRA_R1 (L3) — the fixtures are wrong, not the deployment. The two ship byte-identical
inputs.npzandoutputs.npz(md52d0875bc../a01d0bba..) although their graphs differ: 387 nodes with no quantisation against 389 with 14Dequant. The quantised model is checked against references generated from the float one. Measured error is 0.12–0.50; the5e-2tolerance already in the config was an earlier attempt to absorb exactly this. The fix is to regenerate the references per graph, which needs ORT and belongs in its own PR.ResNet-8 with PromoteTensorsToL2 (SB + DB) — promotion shifts the FP32 accumulation order. The step-0 forward alone is off by 8.7e-4 against a 1e-3 tolerance where plain L3 is bit-exact. Reproduced 3/3 with the same 3-of-4 count, so it is deterministic rather than flaky. MobileNetV1 and CCT-2 take the same path and stay within 1e-4, which makes it look specific rather than inherent. It is off the critical path: ResNet-8's fastest verified configuration is on-chip L2 at 116.2 ms, faster than promote and numerically clean.
Measured
GAP9, tiled, single-buffer, four mini-batches, serial (CI's own markers):
and the two "passes" in the L2 group beforehand were themselves vacuous.
Recovered: DS-CNN (L2), ResNet-8 on-chip (L2), CCT-2, SleepConViT, TSDR, MCUNet. Unchanged and bit-identical: Autoencoder, ResNet-8 and MobileNetV1 in L3, SimpleMLP in L2.
Cycle counts, memory footprints and power numbers measured before this are unaffected — the number of instructions executed does not depend on whether the loss is right — but no correctness claim could rest on
Errors: 0until now.🤖 Generated with Claude Code
https://claude.ai/code/session_012vb2GVs3qbiyLV1KkV2baN