Skip to content

Memoize HermitCrab's sequential analysis cascade#456

Open
johnml1135 wants to merge 1 commit into
masterfrom
feature/memoization
Open

Memoize HermitCrab's sequential analysis cascade#456
johnml1135 wants to merge 1 commit into
masterfrom
feature/memoization

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Ports a memoization scheme (from an archived prototype) for HermitCrab's Unordered-mode analysis cascade: repeated cascade states reached via different rule-unapplication orders are computed once and replayed, instead of re-expanded — this is the source of a well-known ~98% expansion redundancy on template-heavy (Bantu-style) grammars.
  • Off by default. The existing parallel cascade is completely unchanged; you opt in with new Morpher(traceManager, language, maxDegreeOfParallelism: 1), which selects the new sequential+memo path.
  • Acceptance criterion is analysis-set equality (canonical morpheme-signature sets), not byte-identical objects — a memo-replayed Word isn't guaranteed field-for-field identical to a freshly-computed one, even when it represents the same analysis.

See memoization.md (added in this PR) for the full design, the key-completeness audit, and the verification methodology.

Follow-up: #457 stacks a data-structure rearchitecture (array-backed, copy-on-write Shape) on top of this branch and re-measures the same two heavy words — read that PR for whether it's worth the larger diff.

Why off by default — the actual tradeoff

This is not a pure win, and the corpus evidence says so directly:

  • On a known-pathological heavy word (H1), sequential+memo measured 6.2x faster than today's parallel default (isolated to ~6.3x attributable to the memo mechanism itself, via a mutation test separating memo-on/off from sequential/parallel — parallelism alone contributed only ~2% on this word).
  • On typical (non-template) grammars — Indonesian was tested directly, 3 repeated runs to average out noise — sequential+memo runs ~25-28% slower in aggregate than the parallel default: it loses parallelism, and the memo has little order-variant redundancy to collapse there. Forcing the memo off entirely (mutation-tested) widens the slowdown to ~25-47%, confirming most — not all — of this is the lost thread, not the memo itself.
  • On a 70-word Sena slice including one heavy word: 53/69 (77%) of words are individually faster under the memo, 16/69 (23%) slower — but in aggregate wall-clock, the corpus finishes 2.42x faster, because the heavy word dominates the sum. Both of these are true at once and don't contradict each other.

So flipping the library's default cascade mode trades typical-word latency for pathological-word latency. Whether that's the right tradeoff depends on a given corpus's word-difficulty distribution — a decision for a future PR with its own evidence, not this one.

Verification

  • Unit tests: AnalysisStateKey order-invariance/hash/equality, Word.ReplayOnto graft correctness (including a test that specifically distinguishes "grafted the right subtree" from "grafted the wrong one" via distinct lexical entries), the InProgress in-flight re-entry guard, positive-replay-vs-unmemoized-result-set equivalence (including trail order), and an mrule/template equivalence battery against real analysis-rule content via MorpherTests.
  • Corpus runs (local, uncommitted grammars — Sena, Indonesian, Amharic; this repo never commits real grammar/corpus data): zero analysis-set divergences across all three, with both memo tables exercised non-vacuously (hundreds of thousands of hits, not a no-op) — but this excludes the heaviest words, which timed out and never entered the comparison (60 of 312 in the full Sena run). That's exactly the class of word the memo targets, so it's a real gap, not a formality: soundness on the hard cases rests on the two heavy words checked individually below, not on the corpus aggregate.
  • The two heavy words were checked individually to close that gap: H1 (the word above) is 0 divergences on a positive, 2-analysis match. H2 did not complete within a 400s timeout even with the memo on — an inconclusive (not failing) data point, documented in memoization.md rather than glossed over. (Stack RUSTIFY's array/COW rearchitecture on top of memoization #457 revisits H2 with the array-backed rearchitecture and gets a very different result.)
  • Full test suite: 78/78 HermitCrab, 803/806 SIL.Machine (3 pre-existing unrelated skips), 83/83 Thot — all green.

Test plan

  • dotnet build Machine.sln
  • dotnet csharpier check .
  • dotnet test across HermitCrab, SIL.Machine, and Thot test projects
  • Corpus verification harness (MemoCorpusVerification, [Explicit]) run manually against local Sena/Indonesian/Amharic grammars — 0 divergences on the words that completed within the timeout budget

This change is Reviewable

Adds AnalysisStateKey/AnalysisScope/Word.ReplayOnto and wires a memo table
into both the mrule cascade and the affix-template battery, so repeated
analysis-cascade states reached via different rule-unapplication orders are
computed once and replayed rather than re-expanded. Off by default (the
existing parallel cascade is unchanged); opt in via
Morpher(maxDegreeOfParallelism: 1), which selects the new sequential+memo
path.

Ported from an archived prototype (parse-optimization-archive) with
stronger verification: the acceptance gate is analysis-set equality
(canonical morpheme-signature sets), not byte-identical objects, since a
memo-replayed Word is not guaranteed field-for-field identical to a
freshly-computed one. Verified via unit tests (key order-invariance, replay
graft correctness, in-flight re-entry guard) plus corpus runs against three
real grammars (Sena, Indonesian, Amharic) with zero analysis-set
divergences. On a known-pathological word, sequential+memo measured 6.2x
faster than the parallel default (isolated to ~6.3x attributable to the
memo itself, not threading); aggregate corpus evidence and the typical-word
tradeoff are in memoization.md, along with honestly-reported open gaps.
@johnml1135
johnml1135 force-pushed the feature/memoization branch from 5c42eaa to 8f5438a Compare July 17, 2026 11:07
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.14660% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.47%. Comparing base (d9deb16) to head (8f5438a).

Files with missing lines Patch % Lines
....Machine.Morphology.HermitCrab/AnalysisStateKey.cs 73.07% 6 Missing and 8 partials ⚠️
src/SIL.Machine.Morphology.HermitCrab/Morpher.cs 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #456      +/-   ##
==========================================
+ Coverage   73.33%   73.47%   +0.13%     
==========================================
  Files         443      446       +3     
  Lines       37214    37398     +184     
  Branches     5110     5140      +30     
==========================================
+ Hits        27290    27477     +187     
+ Misses       8801     8791      -10     
- Partials     1123     1130       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants