Skip to content

ship/move ordering texel#6182

Merged
matthewevans merged 5 commits into
mainfrom
ship/move-ordering-texel
Jul 19, 2026
Merged

ship/move ordering texel#6182
matthewevans merged 5 commits into
mainfrom
ship/move-ordering-texel

Conversation

@matthewevans

Copy link
Copy Markdown
Member
  • feat(ai): killer-move and PV move ordering for iterative deepening
  • feat(ai): Texel-style self-play eval harvest pipeline

Two-slot per-ply killer moves recorded at beta cutoffs and reordered to
the front of the truncated beam (reorder-only; candidate scores are
never mutated), principal-variation rotation into the next rung gated to
iter_depth >= 1, and RungStat/beta_cutoff/killer_ordering witnesses on
PlannerServices. The iterative-deepening loop is extracted into
run_iterative_deepening with a NaN-safe deterministic pv_argmax.

Root siblings share one per-rung SearchBudget, so PV-first ordering
spends the shared pool on the strongest known candidate before the tail
starves. Includes a serve-reconstruction identity test for the eval
feature extractor (rides here because it lives in planner tests).
EvalFeatures single-authority extractor in eval.rs (breakdown is
reproduced as unweighted features x weights, energy offset applied
post-weighting exactly as before), a drive_game_observed observer seam
in the duel suite with stack-empty-gated last-snapshot-per-turn
harvesting, one HarvestSink per suite run writing JSONL with a single
file-scoped meta line, an ai-duel --harvest flag that forces the
sequential branch, and a --source selfplay path in train_eval_weights.py
(17Lands path untouched) with --allow-tiny-corpus smoke thresholds.

Harvest runs are byte-deterministic for the same (matchup, seed, K);
panicked games label as winner-None and are dropped. Retraining awaits a
real harvest campaign - shipping weights are unchanged.
@matthewevans
matthewevans enabled auto-merge July 18, 2026 22:48

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements self-play eval-feature harvesting for the Texel retrain pipeline, refactors tactical board evaluation to extract unweighted features, and introduces killer-move heuristics and iterative deepening PV threading to optimize search depth. The review feedback highlights a high-severity performance bottleneck in the search hot path due to heap allocations from sort_by_key in order_killers_first, suggesting an in-place stable insertion sort instead. Additionally, a medium-severity issue was identified in the training script where train_test_split can crash on small datasets under --allow-tiny-corpus if stratification is applied to classes with fewer than two members.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +819 to +822
if ranked.iter().any(|r| rank(r) < 2) {
ranked.sort_by_key(rank);
self.killer_orderings += 1;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

[HIGH] Avoid heap allocations on the search hot path. Evidence: crates/phase-ai/src/planner/mod.rs:819-822. Why it matters: Calling sort_by_key on the search tree's hot path allocates heap memory at every node, causing a significant performance bottleneck. Suggested fix: Use a stable in-place insertion sort for the small candidate array.

Suggested change
if ranked.iter().any(|r| rank(r) < 2) {
ranked.sort_by_key(rank);
self.killer_orderings += 1;
}
if ranked.iter().any(|r| rank(r) < 2) {
for i in 1..ranked.len() {
let mut j = i;
while j > 0 && rank(&ranked[j]) < rank(&ranked[j - 1]) {
ranked.swap(j, j - 1);
j -= 1;
}
}
self.killer_orderings += 1;
}
References
  1. Idiomatic Rust — uses the type system, ownership model, and standard library idioms to their fullest. (link)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refuted. The sorted slice is the ranked beam, which is capped at max_branching: 5 in every preset (config.rs — all five presets set 5), and Rust's stable sort_by_key uses its allocation-free insertion-sort path for slices this short; the merge-buffer allocation only engages for long slices. The sort is additionally gated behind ranked.iter().any(|r| rank(r) < 2), so it runs only on the minority of nodes where a killer is actually present in the beam. No hot-path allocation occurs at these lengths.

Comment thread scripts/train_eval_weights.py Outdated
Comment on lines +531 to +543
# train_test_split(stratify=y) requires at least two classes; a single-class
# phase (all wins or all losses) is unlabelable — skip it rather than crash.
if len(np.unique(y)) < 2:
print(
f" WARNING: {phase_name} has a single outcome class — skipping "
"(cannot stratify).",
file=sys.stderr,
)
return None

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[MEDIUM] Prevent crash in train_test_split on small datasets. Evidence: scripts/train_eval_weights.py:531-543. Why it matters: train_test_split with stratify=y raises a ValueError if any class has fewer than 2 members, which will crash the script when running with --allow-tiny-corpus. Suggested fix: Conditionally apply stratification only when all classes have at least 2 members.

Suggested change
# train_test_split(stratify=y) requires at least two classes; a single-class
# phase (all wins or all losses) is unlabelable — skip it rather than crash.
if len(np.unique(y)) < 2:
print(
f" WARNING: {phase_name} has a single outcome class — skipping "
"(cannot stratify).",
file=sys.stderr,
)
return None
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
unique, counts = np.unique(y, return_counts=True)
if len(unique) < 2:
print(
f" WARNING: {phase_name} has a single outcome class — skipping.",
file=sys.stderr,
)
return None
# stratify=y requires at least 2 members per class in train_test_split
stratify = y if all(c >= 2 for c in counts) else None
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=stratify
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — good catch. The single-class skip didn't cover a two-class phase where the least populated class has exactly 1 member, which train_test_split(stratify=y) rejects and which is reachable under --allow-tiny-corpus (per-phase floor is 2). Fixed in 91c5b93: the guard now requires two classes with >=2 members each before splitting, and skips with a warning otherwise.

train_test_split(stratify=y) raises ValueError when the least populated
class has one member - reachable under --allow-tiny-corpus, whose
per-phase floor is 2. Extends the existing single-class skip to require
two classes with >=2 members each. Raised by Gemini review on #6182.
FeatureRow and weighted_total become pub — the JSONL row is the external
trainer contract (scripts/train_eval_weights.py), so public API is the
honest dead-code fix. search_value gets the workspace-precedented
too_many_arguments allow for its 8-arg alpha-beta signature.
@matthewevans
matthewevans added this pull request to the merge queue Jul 19, 2026
Merged via the queue into main with commit bf8dd82 Jul 19, 2026
17 checks passed
@matthewevans
matthewevans deleted the ship/move-ordering-texel branch July 19, 2026 03:54
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.

1 participant