Skip to content

Commit 3cf8be9

Browse files
Address PR review feedback
- Remove dead gc_targets variable and unused n_checked counter - Rename avg_plies_to_forfeit → avg_plies_completed (completed games contribute their full game_length to the average) - Free all GPU tensors in game completion eval cleanup - Move chess_engine import to top of trainer.py - Extract shift_legal_mask() into pawn/data.py to deduplicate the np.roll + zero-fill pattern between data.py and trainer.py - Use math.ceil for fractional CPU counts in cgroup detection
1 parent 84a5bfd commit 3cf8be9

3 files changed

Lines changed: 37 additions & 26 deletions

File tree

pawn/data.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,22 @@ def _watchdog():
240240
step += 1
241241

242242

243+
def shift_legal_mask(mask: np.ndarray) -> np.ndarray:
244+
"""Shift a legal move mask forward by one ply to align with CLM targets.
245+
246+
The engine's legal masks are indexed by position *before* each move:
247+
mask[ply] = legal moves at position ply. But CLM targets[ply] is the
248+
*next* move (= move_ids[ply+1]), so we need legal moves at position
249+
ply+1. This rolls the mask by -1 along the ply axis and zeros the
250+
last entry (no next move at the final ply).
251+
252+
Works for any mask shape (B, T, ...).
253+
"""
254+
shifted = np.roll(mask, -1, axis=1)
255+
shifted[:, -1] = 0
256+
return shifted
257+
258+
243259
def create_validation_set(
244260
n_games: int, max_ply: int, seed: int,
245261
discard_ply_limit: bool = False,
@@ -267,13 +283,10 @@ def create_validation_set(
267283
}
268284

269285
# Compute legal move masks for evaluating legal move rate.
270-
# legal_grid[ply] contains legal moves at the position *before* move_ids[ply],
271-
# but targets[ply] is the *next* move (= move_ids[ply+1]). Shift by one so
272-
# legal_grid[ply] aligns with targets[ply].
286+
# Shift by one ply so legal_grid[ply] aligns with targets[ply]
287+
# (see shift_legal_mask docstring).
273288
legal_grid, _legal_promo = engine.compute_legal_move_masks(move_ids, game_lengths)
274-
legal_grid = np.roll(legal_grid, -1, axis=1)
275-
legal_grid[:, -1, :] = 0 # last ply has no next move
276-
batch["legal_grid"] = torch.from_numpy(legal_grid).long()
289+
batch["legal_grid"] = torch.from_numpy(shift_legal_mask(legal_grid)).long()
277290
batch["game_lengths"] = torch.from_numpy(game_lengths).long()
278291

279292
if no_outcome and prepend_outcome:

pawn/trainer.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
import torch.nn.functional as F
2222
from torch.utils.data import DataLoader
2323

24+
import chess_engine as engine
2425
from pawn.config import CLMConfig, TrainingConfig
2526
from pawn.model import PAWNCLM
26-
from pawn.data import CLMDataset, create_validation_set
27+
from pawn.data import CLMDataset, create_validation_set, shift_legal_mask
2728
from pawn.logging import MetricsLogger
2829

2930
from pawn.data_utils import unpack_grid
@@ -260,26 +261,25 @@ def compute_game_completion(
260261
Returns dict with:
261262
game_completion_rate: fraction of games with zero illegal moves
262263
avg_pct_completion: mean fraction of game completed before forfeit
263-
avg_plies_to_forfeit: mean plies before first illegal move (inf-free)
264+
avg_plies_completed: mean plies completed before first illegal move.
265+
Games with no illegal moves contribute their full game_length.
264266
"""
265267
B, T = preds.shape
266268

267269
with torch.no_grad():
268270
n_complete = 0
269271
pct_completions = []
270-
plies_to_forfeit = []
272+
plies_completed = []
271273

272274
for b in range(B):
273275
gl = min(int(game_lengths[b].item()), T)
274276
forfeit_ply = -1
275-
n_checked = 0
276277
for p in range(gl):
277278
if not loss_mask[b, p]:
278279
continue
279280
# Skip plies with no legal moves (end-of-game padding)
280281
if not legal_mask[b, p].any():
281282
continue
282-
n_checked += 1
283283
token = int(preds[b, p].item())
284284
if token < legal_mask.shape[2] and not legal_mask[b, p, token]:
285285
forfeit_ply = p
@@ -291,15 +291,15 @@ def compute_game_completion(
291291
if forfeit_ply < 0:
292292
n_complete += 1
293293
pct_completions.append(1.0)
294-
plies_to_forfeit.append(float(gl))
294+
plies_completed.append(float(gl))
295295
else:
296296
pct_completions.append(forfeit_ply / gl if gl > 0 else 0.0)
297-
plies_to_forfeit.append(float(forfeit_ply))
297+
plies_completed.append(float(forfeit_ply))
298298

299299
return {
300300
"game_completion_rate": n_complete / B if B > 0 else 0.0,
301301
"avg_pct_completion": sum(pct_completions) / len(pct_completions) if pct_completions else 0.0,
302-
"avg_plies_to_forfeit": sum(plies_to_forfeit) / len(plies_to_forfeit) if plies_to_forfeit else 0.0,
302+
"avg_plies_completed": sum(plies_completed) / len(plies_completed) if plies_completed else 0.0,
303303
}
304304

305305

@@ -582,10 +582,8 @@ def evaluate(self) -> dict[str, float]:
582582
# without picking an illegal move? Uses a small subset to avoid
583583
# materializing a large dense (B, T, vocab) token mask.
584584
if "game_lengths" in self.val_data:
585-
import chess_engine as engine_mod
586585
gc_n = min(64, n)
587586
gc_input = self.val_data["input_ids"][:gc_n].to(self.device)
588-
gc_targets = self.val_data["targets"][:gc_n].to(self.device)
589587
gc_loss_mask = self.val_data["loss_mask"][:gc_n].to(self.device)
590588
gc_game_lengths = self.val_data["game_lengths"][:gc_n].to(self.device)
591589
move_ids = self.val_data["input_ids"][:gc_n].numpy().astype(np.int16)
@@ -598,18 +596,17 @@ def evaluate(self) -> dict[str, float]:
598596
gc_logits = model.lm_head(hidden)
599597
gc_preds = gc_logits.argmax(dim=-1)
600598

601-
# Dense legal token mask, shifted to align with targets
602-
legal_tokens = engine_mod.compute_legal_token_masks(move_ids, gl_np, vocab_size)
603-
legal_tokens = np.roll(legal_tokens, -1, axis=1)
604-
legal_tokens[:, -1, :] = False
605-
legal_mask_t = torch.from_numpy(legal_tokens).to(self.device)
599+
legal_tokens = engine.compute_legal_token_masks(move_ids, gl_np, vocab_size)
600+
legal_mask_t = torch.from_numpy(
601+
shift_legal_mask(legal_tokens)
602+
).to(self.device)
606603

607604
gc = compute_game_completion(gc_preds, legal_mask_t, gc_loss_mask, gc_game_lengths)
608605
avg["val/game_completion_rate"] = gc["game_completion_rate"]
609606
avg["val/avg_pct_completion"] = gc["avg_pct_completion"]
610-
avg["val/avg_plies_to_forfeit"] = gc["avg_plies_to_forfeit"]
607+
avg["val/avg_plies_completed"] = gc["avg_plies_completed"]
611608

612-
del legal_mask_t, gc_logits, gc_preds
609+
del gc_input, gc_loss_mask, gc_game_lengths, legal_mask_t, gc_logits, gc_preds
613610
if self.device != "cpu" and torch.cuda.is_available():
614611
torch.cuda.empty_cache()
615612

@@ -705,7 +702,7 @@ def _graceful_exit(signum, frame):
705702
if "val/game_completion_rate" in val_metrics:
706703
val_msg += (
707704
f" | complete {val_metrics['val/game_completion_rate']:.3f}"
708-
f" | avg_ply {val_metrics['val/avg_plies_to_forfeit']:.0f}"
705+
f" | avg_ply {val_metrics['val/avg_plies_completed']:.0f}"
709706
)
710707

711708
# Compound early stopping

scripts/benchmark.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,12 +1095,13 @@ def _collect_cpu_cache() -> dict[str, str]:
10951095

10961096
def _cgroup_cpu_count() -> int | None:
10971097
"""Return container CPU limit from cgroups, or None if unconstrained."""
1098+
import math as _math
10981099
# cgroup v2: cpu.max contains "quota period" (e.g. "200000 100000" = 2 CPUs)
10991100
try:
11001101
text = Path("/sys/fs/cgroup/cpu.max").read_text().strip()
11011102
quota_s, period_s = text.split()
11021103
if quota_s != "max":
1103-
return max(1, int(int(quota_s) / int(period_s)))
1104+
return max(1, _math.ceil(int(quota_s) / int(period_s)))
11041105
except (OSError, ValueError):
11051106
pass
11061107

@@ -1109,7 +1110,7 @@ def _cgroup_cpu_count() -> int | None:
11091110
quota = int(Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").read_text().strip())
11101111
if quota > 0:
11111112
period = int(Path("/sys/fs/cgroup/cpu/cpu.cfs_period_us").read_text().strip())
1112-
return max(1, int(quota / period))
1113+
return max(1, _math.ceil(quota / period))
11131114
except (OSError, ValueError):
11141115
pass
11151116

0 commit comments

Comments
 (0)