Measure where HermitCrab's time actually goes, across 33 grammars - #488
Closed
johnml1135 wants to merge 15 commits into
Closed
Measure where HermitCrab's time actually goes, across 33 grammars#488johnml1135 wants to merge 15 commits into
johnml1135 wants to merge 15 commits into
Conversation
Rebases this line of work onto integrate-conformance-framework rather than master: it carries 33 committed grammars (8 typologically distinct languages plus 25 edge cases) with hand-derived expected outputs and a Fixture.DiscoverAll enumerator. Every prior conclusion here came from three grammars, and the last one died because a result that looked general was Sena-shaped. The reframing under test: forward synthesis is not order-sensitive search, it is a deterministic fold driven by the trail (Word.IsMorphologicalRuleApplicable admits only _mruleApps[_mruleAppIndex]). The end of the trail is applied FIRST in synthesis, and ReplayOnto's mruleTrailPrefixLength already marks that boundary -- so the analysis memo has already computed which candidates share a synthesis-first segment, and that segment is currently re-folded per path, per root, per alternative. Folds can be shared by identical subsequence and by computed value, neither of which assumes commutativity, so rule non-commutativity stops being a blocker and becomes something detected. Carries over the predecessor branch's three evidence docs unchanged and the RuleLengthClassifier (orphaned there by the Stage-0 stop, reused by probe P2). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t histogram, fold-step fingerprint Adds SynthesisProbe, a gated (Enabled defaults false, so the ordinary sequential-path test suite pays nothing) static instrumentation hub for the three P1 measurements in docs/hermitcrab-synthesis-fold-probes.md section 3: - P1a wall-time split: cascade/template-battery timers in SynthesisStratumRule, LexicalLookup and a derived forward-synthesis residual in Morpher.SynthesizeSequential. - P1b die-point histogram: unconditional (not IsTracing-gated) counters at each rejection site in SynthesisAffixProcessRule, SynthesisRealizationalAffixProcessRule, and Allomorph.IsWordValid, reusing FailureReason's vocabulary where it lines up. - P1c fold-step fingerprint: a dedicated (fingerprint, applied rule) key, deliberately not Word.ValueEquals (which omits SyntacticFeatureStruct, MPR features, and disjunctive allomorph indices -- see Word.cs:600), plus a determinism check on repeat keys. Word.cs gains three read-only accessors (AppliedRuleCounts, DisjunctiveAllomorphIndices, PendingTrailPosition) mirroring the existing UnappliedRuleCounts pattern, needed for the P1c fingerprint. No behavior change: full non-Explicit HermitCrab suite still green (582 passed).
…eterminism check
SynthesisFoldProbe.cs is the P1 harness: Probe_ConformanceFixtures runs all 33 conformance
fixtures with no env vars (synthetic, committed, safe to print); Probe_RealCorpus is env-var
driven exactly like MemoCorpusVerification (HC_MEMO_GRAMMAR/HC_MEMO_WORDS/HC_MEMO_MAX_WORDS)
plus HC_PROBE_WORDS for an explicit word list. Both report per-word P1a/P1b/P1c lines plus a
per-fixture summary, never pooling a ratio across fixtures.
First run against the 33 fixtures surfaced 130 "determinism violations" -- investigation showed
these were a bug in the check itself, not the fingerprint: SynthesisProbe.RecordApplication was
called once per successful allomorph inside SynthesisAffixProcessRule/SynthesisRealizationalAffixProcessRule's
per-allomorph loop, so ordinary disjunctive fan-out (several allomorphs of the same rule all
matching one input before the environment/free-fluctuation break) looked like the same
(fingerprint, rule) key producing different outcomes on different calls. This is exactly the
set-valued-partial shape the plan doc's second trap already names for realizational rules ("any
stored partial must be a set, like MemoEntry.Results, not a value") -- it just also applies to
ordinary affix rules' allomorph loops. Fixed by recording once per Apply() call with the whole
output list (RecordApplications), comparing outcome SETS via bipartite FingerprintEquals matching
instead of single values. Re-run: 0 violations across all 33 fixtures.
Also broadened SynthesisFoldProbe.ProbeWord's catch to any Exception, not just
InvalidShapeException: some edge-case fixtures have ExpectCrash: true (e.g.
simultaneous-epenthesis-cascade throws InfiniteLoopException by design), and this probe isn't the
self-check for that contract -- it now notes the crash and skips measurement for that word.
…logy-dependent Zero determinism violations across all 33 fixtures: equal fingerprint plus equal applied rule never produced a different outcome multiset, across 8 typologies and 25 edge cases. That is the check that would have exposed an incomplete fingerprint, so the ratios below are real sharing rather than collisions. P1c ranges from 8.10x (suffixing-evidential-adjacency-chain) and 3.22x (deep-optional-affix-nesting, the largest sample at 5,556 applications) down to 1.11x (metathesis) and exactly 1.00x (mpr-overwrite-order-dependence). The split is itself a sanity check: the fixture built to be order-dependent shares nothing, a suffix chain shares 8x. The measurement discriminates in the direction the mechanism predicts. Two cautions recorded on P1b: it counts rejection EVENTS, not candidates, and is NOT the same denominator as the historical 218,847 figure; and its dominant bucket is an O(1) trail-position check, so a count histogram overstates its cost share. Cost-weighting is required before it becomes a build decision. P1a is unreliable at sub-2ms fixture scale, but the one large fixture puts the template battery at 67.4% of 2,393ms -- matching the historical Sena finding. Full HC suite 582 passed / 1 skipped / 0 failed with the probe present. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P1c on Sena is 265.5x with zero determinism violations -- the >=5x gate cleared by a wide margin. On Sena that does not convert, because forward synthesis is 0.2% of wall time and all four instrumented buckets total 5.0%. Separates two tangled findings. (i) A measurement defect: every timer landed on the synthesis side (SynthesisStratumRule.cs:107/:136); the analysis phase was never instrumented. That is an ambiguity in the brief -- a cascade and a template battery exist on both sides. (ii) A real result: even granting the defect, Sena heavy-word time is overwhelmingly analysis, and cinacemerwa's 218,847 synthesis inputs cost ~0.5us each. Counting candidates told us where the volume was, never where the time was -- exactly the row the predecessor plan required and skipped. Corrects the framing that mattered most: Sena being analysis-bound is a fact about Sena, not a verdict on the technique. Value per grammar is the PRODUCT of the sharing ratio and the forward-synthesis share of wall time. We have the first across 33 fixtures; we have the second only for Sena. No conclusion about any other grammar is licensed until the second factor is measured per grammar. Flags Amharic as the priority: 212 states and 186 synthesis inputs costing 30 seconds is ~160ms per synthesis run against Sena's ~0.5us -- five orders of magnitude apart. If Amharic is synthesis-bound, Sena is the outlier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ts, add unaccounted
The four P1a timers all landed on the synthesis side (synCascade/synBattery, both
renamed from cascade/templateBattery; synForward, renamed from forwardSynthesis) --
95% of Sena heavy-word wall time was unaccounted for because nothing on the
analysis side was ever instrumented.
Adds four new SynthesisProbe buckets, all instrumentation-only and gated on
SynthesisProbe.Enabled (default false):
- anTotal: outer/nested bucket around Morpher.ParseWord's
_analysisRule.Apply(input) call -- the whole analysis phase.
- anCascade: AnalysisStratumRule.ApplyMorphologicalRules' _mrulesRule.Apply
call (MemoizedCombinationRuleCascade for the memoized Unordered path this
harness always runs; also covers PermutationRuleCascade for Linear strata
and ParallelCombinationRuleCascade if ever reached at this same call site).
- anBattery: AnalysisStratumRule.ApplyTemplates' call to
ApplyTemplateBattery, covering both the memoized and unmemoized paths.
- anPhono: AnalysisStratumRule.Apply's _prulesRule.Apply(input) call, the
analysis phonological-rule cascade.
anCascade/anBattery/anPhono are disjoint exclusive slices (none of their call
sites re-enter each other); anTotal is a nested/inclusive total that contains
all three plus whatever analysis-side orchestration they don't individually
bracket. SynthesisFoldProbe.cs now reports both an exclusive top-level split
(lookup/synCascade/synBattery/synForward/anTotal/unaccounted, summing to wall)
and an anTotal-internal breakdown (anCascade/anBattery/anPhono/anOther), with
the nesting scheme stated in the header. Also adds a per-fixture
forward-synthesis-share x P1c-ratio "value" column to Probe_ConformanceFixtures,
since P1c sharing can only be realized as a speedup on the forward-synthesis
slice of wall time.
Full HermitCrab suite still green: 582 passed / 1 skipped / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ong phase Per-grammar results with trustworthy timers (Amharic unaccounted 0.1%). Max possible speedup = share x (1 - 1/ratio), NOT ratio x share. Best anywhere is 22.5% (feature-system-breadth, 1.60x on a 60.1% synthesis share); Sena's 265x is worth 0.2% because 265x of nothing is nothing. Refutes this document's own earlier claim that Amharic was synthesis-bound: the '160ms per synthesis run' for the word at 6.3 was arithmetic on an unmeasured denominator. Its forward synthesis is 14ms; its 36 seconds are in the analysis morphological-rule cascade at ~170ms per state. Fixtures show 15-60% synthesis share against 0.1-0.2% for real grammars. Evidence favours grammar size over typology as the explanation: Sena is agglutinative and is 0.2%, and synthesis share falls monotonically as fixtures get larger. Verdict: do not build fold sharing. Redirects to the analysis cascade -- 99.5% of Amharic, already memoized, already at its state floor, and still ~170ms per state. Every optimization so far reduced node COUNT; none touched per-node COST. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ar-dependent Sena pooled: anBattery 51.4%, anCascade 18.4%, synthesis ~5%, unaccounted 20.1%. Amharic: anCascade ~95% of anTotal, anBattery 4%. So Sena is template-battery- bound and Amharic is cascade-bound; neither is synthesis-bound. Corrects this document's own previous claim that the cascade was 'the target', which was drawn from Amharic alone -- the Sena-shaped error in reverse. Notes that the template battery is STILL 51.4% of Sena after memoization (Phase 3b measured 93% pre-memo, 5x from its memo): the memo reduced how often the battery runs, not what a run costs. The common thread across both grammars is per-node cost, not node count. Every optimization tried in this area -- memoization, key narrowing, lexical gating, tandem intersection -- reduced how many nodes are visited. None touched what a node costs. Records Sena's 20.1% unaccounted as an open gap with a named hypothesis (Word.ExpandAlternatives outside every timed region), flagged as hypothesis not measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adversarial review found the ceiling table divided by the wrong share, confirmed in code. synForward is explicitly net of cascade and battery (Morpher.cs:424), but every application P1c counts is recorded inside SynthesisAffixProcessRule. Apply / SynthesisRealizationalAffixProcessRule.Apply, which run INSIDE the synCascade and synBattery brackets -- template slot rules compile to those same classes via RuleBatch. Shareable work lives in synCascade+synBattery+synForward; the table divided by the one bucket that excludes it. Sena: 0.2% -> ~4.98%. Amharic: 0.07% -> ~0.16%. Withdraws the fixture ceiling column and the 'best anywhere 22.5%' headline: it was computed from fixture timings section 6.1 itself declared unreliable, which made the doc self-contradictory. Walks back what 0 determinism violations proves. RecordApplications returns early on empty output, so a (fingerprint, rule) pair that succeeds once and fails once is never compared; and outcomes are compared with the same FingerprintEquals used to key them, which covers trail POSITION but not remaining-trail CONTENT. Zero violations licenses per-step decision determinism only, not soundness of a build. Walks back the size-vs-typology dismissal: its premise was the wrong Sena number, the large end of the trend is one fixture whose size is analysis-caused, and trail-exempt realizational branching is a concrete mechanism for a synthesis-bound family none of our grammars represent. Scopes the verdict to parsing: GenerateWords is pure synthesis and unmeasured. Records trail-position indexing as measured-worthless (0.2%) so it does not become a default task. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a synExpand tick bucket (SynthesisProbe.AddSynExpandTicks/SynExpandTicks) around the ExpandAlternatives() call in Morpher.SynthesizeSequential -- the only call site the probe harness reaches, since guessRoot=false and the sequential path is the only one this harness exercises. synExpand is a new exclusive top-level slice, so unaccounted shrinks by exactly what it gains. Also adds a dedupe census at fold entry: for every alternative about to enter _synthesisRule.Apply, counts total alternatives, distinct alternatives by the existing P1c fingerprint (SynthesisProbe.AlternativeKey reuses FingerprintHash/FingerprintEquals, no second fingerprint), and dupe provenance -- whether a duplicate's first occurrence traces to the same analysis word or a different one, via reference-identity against the outer loop variable already in scope. That split decides interceptability: same- analysis-word duplication is catchable before ExpandAlternatives' Clone/Unify/Freeze work; cross-analysis-word duplication only after it. SynthesisFoldProbe.cs reports all of this per word and cumulatively per fixture/corpus, alongside the existing P1a/P1b/P1c columns. Instrumentation only, gated on SynthesisProbe.Enabled (false by default), no control-flow changes. Full suite reconfirmed green: 582 passed, 1 skipped, 0 failed.
…-sharing verdict stands, new dedupe-at-fold-entry candidate found Sena unaccounted collapses 20.1% -> 1.5% pooled (24.0% -> 2.01% on cinacemerwa, 7.6% -> 0.77% on atawirambo) once synExpand is broken out. Hypothesis confirmed against the <5% gate. The 6.4 fold-step-sharing verdict for parsing does not flip: ExpandAlternatives runs outside the fold P1c measures, and synCascade+synBattery+synForward is still ~5% of wall (ceiling 5.32%, matching 6.4's 4.98%). The dedupe census at fold entry finds a second, larger, gated-ON opportunity: 395,026 alternatives collapse to 61 distinct pooled (0.02%), and 85.0% of duplicates (73.0%-100% per word) trace to the same analysis word -- the OFF condition (predominantly cross-word) never fires. Estimated ceiling ~17.3% of Sena wall time, an order of magnitude above the fold-step build. Reported as a measurement and a scoped next candidate, not built.
The hypothesis result stands: unaccounted 20.1% -> 1.5%, synExpand = 20.3% of Sena wall time, invisible to every prior instrumentation round. Denominators cross-check exactly against an independent run. The census conclusion does not. 395,026 -> 61 distinct is measured with the P1c fingerprint, which carries PendingTrailPosition (an int index) and no remaining-trail content. Defensible for a fold STEP where the continuation is re-anchored; not for fold-ENTRY dedupe, where skipping an alternative discards its distinct continuation -- lost parses. This measurement already exists: same call sites, same 218,847 denominator, on parse-forest-tandem. F1 v1 got 9,774x with a naive key and it was proven unsound; F1 v2 got 28.72x with 2 residual violations; F2 shipped fully order-sound at 15-40%. The 6,476x is the 9,774x again, and the adversarial review named this exact failure mode one step earlier. Corrected ceiling: 20.3% x 15-40% = 3-8% of Sena wall. Still the largest opportunity surfaced so far, on an axis nobody had instrumented, but a fifth of the claimed figure and it needs a trail-complete key first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ceilings Re-run with synTotalShare = (synCascade+synBattery+synForward)/wall and a reliability flag. deep-optional-affix-nesting: 3.22x on a 73.1% share = 50.4% ceiling, on the largest and most trustworthy timing sample in the set (2.8s). suffixing-evidential-adjacency-chain: 8.10x on 59.3% = 52.0% ceiling. REFUTES this document's own size-vs-typology argument. Section 6.4 cited deep-optional-affix-nesting as the largest fixture with the lowest synthesis share (6.1%) -- but that 6.1% was forwardShare, the wrong denominator. Its real synTotalShare is 73.1%, the highest of any reliable row. The largest, most reliable fixture is the MOST synthesis-bound, not the least. The trend the argument rested on does not exist. Also: deep-optional-affix-nesting has altTotal 926 -> altDistinct 3 (0.32%) with dupeSameWordPct = 100.0%, so every duplicate shares a trail and the ExpandAlternatives dedupe is fully interceptable pre-expansion there. Records the category verdicts: A (affix template slots) clears with both reliable rows; B/C/D clear on mechanism but sit on sub-12ms fixtures; E discarded as one observation; F-K fail and are retained as evidence for future grammars. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both reliable >=2x fixtures reproduce to within ~1.7 points, both at ~50% ceiling: deep-optional-affix-nesting 50.41%/50.94%, suffixing-evidential 51.95%/50.23%. That is the result the build rests on and it is stable. The unreliable rows do not reproduce: strrep-identity's synExpandShare moved 1.3% -> 14.5% on a 10ms fixture. Direct evidence for the wallMs>=50 flag rather than an assumption behind it, and the reason groups C and D must be read as 'the mechanism engages' and never as speedup estimates. Deterministic counters were byte-identical across both runs; 0 determinism violations in both. Notes that 1a7d484 bundles two concerns, and why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ot retry One row per optimization attempted across this whole line of work, with what was expected, why it failed, and the number that settled it. Deliberately in-repo and durable rather than evicted into a PR accordion: a closed avenue is only closed if the next person can find out cheaply that it is closed. Three of these have already been independently rediscovered and re-retracted -- the order-insensitive dedupe number (9,774x) has now been re-derived twice by different routes. That is the failure mode this file exists to prevent. Records the generalising lesson: the redundancy in HermitCrab's synthesis is apparent, not real. Three measurements at three boundaries all collapsed once the key carried the trail (9,774x -> 15-40%, 6,476x -> unestablished, 3.22x/8.10x -> hits=0). That closes packed forests, fold sharing and input dedupe as a family, not as three candidates. Records where the time is (Sena: battery 51.4%, ExpandAlternatives 20.3%, synthesis ~5%; Amharic: analysis cascade ~95%, synthesis 0.3%) and the six method rules earned here, including the three retracted findings and their common cause. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 27, 2026
Collaborator
Author
|
Superseded by #490 (docs-only). Instrumentation stays on |
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.
HermitCrab has been optimised repeatedly without anyone measuring where its time goes. This branch measures it — across 33 grammars rather than three — and the answer redirects what we should build next.
What it adds. A wall-time split of
ParseWordinto eight exclusive buckets that sum to wall (analysis total, analysis cascade / battery / phonology, lexical lookup, synthesis cascade / battery / forward,ExpandAlternatives), a die-point histogram for rejected synthesis candidates, and a fold-step sharing census. All behind onevolatile bool, false in normal operation; every edit is an insertion before an existingreturnorcontinue. No behaviour change: 582 passed / 1 skipped / 0 failed, including every conformance fixture gate.Where the time is. Not synthesis. Synthesis is ~5% of Sena and 0.3% of Amharic. The cost is analysis, and which part differs by grammar: Sena is affix-template-battery-bound (51.4%, still, after the memo that bought 5x) plus
ExpandAlternativesat 20.3% — a bucket nothing had ever instrumented. Amharic is cascade-bound (~95%), at ~170 ms per analysis state in a cascade already at its state-count floor.The finding that closes a family. Forward synthesis is a deterministic trail-driven fold, and the sharing looked large: 3.22x and 8.10x, ~50% wall ceilings on the two fixtures big enough to time reliably, reproduced across two independent runs. It was built (stacked branch, parity clean, 0 divergences on all 33 fixtures) and it does not pay —
hits = 0on the best fixture, 0.96x, i.e. 4% slower from key construction alone. A sound key must carry the ordered remaining trail, and two candidates then have to agree on their whole future to share a step.That is the third independent route to the same result. The redundancy is apparent, not real: the trail is what makes each step distinct, and every measurement showing large shareable work is measuring a key that omits it. Packed forests, fold sharing and synthesis-input dedupe all need distinct derivations to converge on a genuinely identical state; in this engine they do not.
What to review.
docs/hermitcrab-optimization-ledger.mdis the deliverable — 20 rows, tried / closed / open, with the number that settled each. The instrumentation is mechanical; the judgement is in the ledger.Note on process: the research docs are deliberately kept in-repo, not evicted into accordions. Three of these avenues have already been independently rediscovered and re-retracted — the 9,774x dedupe figure has been re-derived twice by different routes. A closed avenue is only closed if the next person finds out cheaply that it is closed.
Category results — every group at the 2x bar
Ratios are deterministic and reproduce byte-identically across runs. Only
wallMs >= 50rows can carry a timing claim: one 10 ms fixture's share moved 1.3% to 14.5% between two runs of identical code.The categories are real — A and B have a clean mechanistic reason to share, and the fixture built to be order-dependent reports exactly 1.00x, which is the mechanism failing where it should. The 2x was real as available sharing. It is not extractable, because what makes it visible is what makes it unsound.
F through K are retained rather than deleted: they are the reference for judging a future grammar, and a category that fails on today's fixtures may pass on a grammar we do not yet have.
Corrections made during this work
Three findings were published inside this branch and then withdrawn on measurement. All three were arithmetic on an unmeasured denominator.
synForward, which is explicitly net of the cascade and battery buckets while the shared work runs inside them. Its real share is 73.9% — the highest of any reliable row. The trend the argument rested on does not exist.A fourth was caught in the harness rather than published: a first A/B design showed 1.53x purely from JIT warm-up, with the off-arm varying 42.7% between its own first and second sample. The harness now discards warm-up, takes min of N interleaved samples per arm, and prints the off-arm spread beside every speedup.
The wall-time split this branch adds is what makes all of these checkable. It was a required row in a predecessor plan and was skipped.
Durable constraints, and the retained classifier
Two findings are in-repo because the code cannot express them:
Word.ReplayOntodoes not splice_mrulesUnapplied. Safe only becauseAnalysisStateKeyincludes the per-rule count multiset, which guarantees arrival and stored-arrival counts match on a hit. Anyone narrowing that key breaks this silently. Fix if needed: store arrival counts onMemoEntryand computestored - storedArrival + query.RuleLengthClassifieris retained with 12 tests and no callers. It encodes a direction trap that is easy to get backwards: on the analysis side rules are un-applied, so a rule that inserts on synthesis removes on analysis, and one that truncates on synthesis untruncates — an Lhs part with no Rhs copy makes the un-applied word grow.AnalysisMorphologicalTransform.GenerateShapeis the authority. Removing it is defensible; keeping it means the next attempt at length-based pruning will not get the sign wrong.Commit
1a7d484cbundles two concerns — a harness reporting change and a docs analysis — because the change was swept in from an uncommitted state. Noted rather than rewritten.Next: per-node cost
Every optimization in the ledger that worked reduced how many nodes are visited. None touched what a node costs. Amharic spends ~170 ms per analysis state in a cascade already at its state floor — the state count is a floor, and the cost per state is unexplored. That decomposition (FSA pattern matching vs feature unification vs clone/freeze vs key hashing) is the next probe, and it is in flight.
This change is