Skip to content

Commit 989f795

Browse files
Shrink the deferred-reduction decision record
Measuring reducer-attributable peak memory (tracemalloc around the _reduce call, four complex sentences) showed the deferred-reduction decision record costing 3.8 MB against 1.4 MB for the previous reduce-while-scoring approach. Notably, the early culling itself was not what kept the old peak low - culled child nodes stay pinned as visited-dict keys until go() returns regardless - the difference was purely the size of the decision record. Trim it in two ways. First, only the winning family's child list is retained per (node, context); the losers' lists are never referenced by the reduction pass (for no_reduce nonterminals, which the pass descends into fully, the families are flattened into one list). Second, an entry is only stored at all when the reduction pass cannot infer it from the node: a real choice between multiple families, or children scored under a context-sensitive signature. For the common case - a single-family node whose children are all context-free - the pass walks the node's own child list, deriving each child's signature from its kind (None for tokens, NEUTRAL otherwise; the storage condition guarantees these derivations match what the scoring pass used). Reducer-attributable peak drops to 2.1 MB on the same benchmark, with byte-identical reduction output on the 30-sentence regression corpus and unchanged wall time. 139 tests pass; mypy and ruff clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0cb37d0 commit 989f795

1 file changed

Lines changed: 44 additions & 15 deletions

File tree

src/reynir/reducer.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ class ResultDict(TypedDict, total=False):
127127
BonusCache = Dict[Tuple[BIN_Terminal, str, BIN_Terminal, BIN_Token], int]
128128
FinalsDict = Dict[int, Set[BIN_Terminal]]
129129
TokensDict = Dict[int, BIN_Token]
130-
# Winning family index and per-family lists of (child, context signature)
131-
DecisionTuple = Tuple[int, List[List[Tuple[Node, Any]]]]
130+
# Winning family index and the (child, context signature) pairs
131+
# that the reduction pass should descend into
132+
DecisionTuple = Tuple[int, List[Tuple[Node, Any]]]
132133

133134
# Reducer result dictionary with a null score, shared between
134135
# empty nodes; wrapped in a read-only proxy so that it cannot
@@ -467,7 +468,26 @@ def calc_score(w: Node, sig: Any) -> ResultDict:
467468
# (derivation), including an "sc" field for its score,
468469
# along with the winning family index
469470
v, chosen_ix = scope.process(w)
470-
decisions[(w, sig)] = (chosen_ix, fam_children)
471+
nt = w.nonterminal if w.is_completed else None
472+
if nt is not None and nt.no_reduce:
473+
# This node will not be reduced: the reduction pass
474+
# descends into the children of every family
475+
decisions[(w, sig)] = (
476+
chosen_ix,
477+
[pair for fam in fam_children for pair in fam],
478+
)
479+
elif len(fam_children) > 1 or any(
480+
chsig is not None and chsig != NEUTRAL
481+
for _, chsig in fam_children[0]
482+
):
483+
# Only record what the reduction pass can't infer:
484+
# an actual choice between families, or children
485+
# scored in a context-sensitive signature. For a
486+
# single-family node whose children are all
487+
# context-free, the walk derives the same
488+
# information from the node itself, saving the
489+
# memory of recording it here.
490+
decisions[(w, sig)] = (chosen_ix, fam_children[chosen_ix])
471491
else:
472492
v = NULL_SC
473493
# Memoize the result for this (node, context) combination
@@ -489,22 +509,31 @@ def apply_reduction(w: Node, sig: Any) -> None:
489509
reduced.add(w)
490510
entry = decisions.get((w, sig))
491511
if entry is None:
492-
# Token or empty node: nothing to reduce
512+
if w._token is not None or not w.is_span or not w._families:
513+
# Token or empty node: nothing to reduce
514+
return
515+
# Single-family node with context-free children, not
516+
# recorded by the scoring pass: nothing to reduce, and
517+
# each child's signature can be derived from its kind
518+
# (None for tokens, NEUTRAL for everything else - see
519+
# the storage condition in calc_score)
520+
_, children0 = w._families[0]
521+
for ch0 in children0:
522+
if ch0 is not None:
523+
apply_reduction(
524+
ch0, None if ch0._token is not None else NEUTRAL
525+
)
493526
return
494-
chosen_ix, fam_children = entry
527+
chosen_ix, children = entry
495528
nt = w.nonterminal if w.is_completed else None
496-
if nt is not None and nt.no_reduce:
497-
# Leave the child families of this nonterminal in place;
498-
# this feature is used in query processing
499-
for fam in fam_children:
500-
for ch, chsig in fam:
501-
apply_reduction(ch, chsig)
502-
else:
529+
if nt is None or not nt.no_reduce:
503530
# The key action of the reducer:
504-
# eliminate all families except the winning one
531+
# eliminate all families except the winning one.
532+
# (Nonterminals tagged no_reduce keep all their child
533+
# families; this feature is used in query processing.)
505534
w.reduce_to(chosen_ix)
506-
for ch, chsig in fam_children[chosen_ix]:
507-
apply_reduction(ch, chsig)
535+
for ch, chsig in children:
536+
apply_reduction(ch, chsig)
508537

509538
# First pass: score the forest without modifying it
510539
root_sig = context_sig(root_node)

0 commit comments

Comments
 (0)