Skip to content

fix: stop double-advancing the dataset cursor (half the dataset never trained) - #1

Open
golldyck wants to merge 1 commit into
NousResearch:mainfrom
golldyck:fix/dataset-cursor-double-advance
Open

fix: stop double-advancing the dataset cursor (half the dataset never trained)#1
golldyck wants to merge 1 commit into
NousResearch:mainfrom
golldyck:fix/dataset-cursor-double-advance

Conversation

@golldyck

@golldyck golldyck commented Jul 1, 2026

Copy link
Copy Markdown

The bug

run() advances the dataset cursor once per step:

# trainer.py, run()
self.dataset_cursor += self.cfg.training.batch_size

but _pick_batch also added step * batch_size on top of that same cursor:

start = (self.dataset_cursor + step * batch_size) % len(self.dataset)

The cursor already moves each step, so the start offset advances by 2 * batch_size per step:

step cursor at call start (buggy)
0 0 0
1 b 2b
2 2b 4b

Batches begin at 0, 2b, 4b, …. Every other slice of the dataset never gets trained on, and the traversal wraps after covering only half the data. Simulated over a 20-row dataset with batch_size=2, the rows ever touched are {0,1,4,5,8,9,12,13,16,17}; rows 2,3,6,7,… never get picked. Resume makes it worse. The restored cursor and the step term double-count again, so a resumed run doesn't continue contiguously either.

Root cause

The original trainer used start = (step * batch_size) % len. The resume-state change added the per-step dataset_cursor += batch_size in run() but left the step term in _pick_batch in place, so the advance now happens twice.

Fix

dataset_cursor is the single source of truth for the next offset. It advances each step and is restored from the checkpoint on resume, so _pick_batch should use it alone:

start = self.dataset_cursor % len(self.dataset)

The step parameter is now unused, so it's removed and its only caller updated.

Tests

Adds two regression tests to tests/unit/test_trainer.py that mirror the run() loop (call _pick_batch(), then advance the cursor by batch_size):

  • test_pick_batch_covers_dataset_contiguously: a fresh run trains rows 0,1,2,… with no gaps (fails on the old code with [0,1,4,5,8,9,…]).
  • test_pick_batch_stays_contiguous_after_resume: a cursor restored to 20 continues 20,21,22,… (fails on the old code with [20,21,24,25,28,29]).

run() advances self.dataset_cursor by batch_size after every step, but
_pick_batch ALSO added step * batch_size on top of the cursor. Because the
cursor already moves each step, the start offset advanced by 2 * batch_size
per step: batches began at 0, 2b, 4b, ... and every other slice of the
dataset was silently never trained on (the traversal wrapped after covering
only half the data). Resume made it worse — the restored cursor and the
step term double-counted again.

The original trainer used start = (step * batch_size) % len; the resume-state
change introduced the per-step cursor increment without dropping the step
term. Fix: dataset_cursor alone is the start offset (it advances each step
and is restored on resume), so drop the step term and the now-unused
parameter. Adds regression tests asserting contiguous coverage for both a
fresh run and a resumed cursor.
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