geekcomputers/Python · error · ValueError

Expected non-negative epoch, but got {}

Error message

Expected non-negative epoch, but got {}

What it means

This ValueError is raised by the cosine-annealing-with-warm-restarts scheduler's step(epoch) when an explicit negative epoch value is passed. When epoch is not None the scheduler computes the position within the annealing cycle directly from it, so a negative epoch has no meaningful interpretation and is rejected before the logarithmic restart math runs.

Source

Thrown at ML/src/python/neuralforge/optim/schedulers.py:50

        self.T_i = T_0
        super().__init__(optimizer, last_epoch)
    
    def get_lr(self):
        return [
            self.eta_min + (base_lr - self.eta_min) * (1 + math.cos(math.pi * self.T_cur / self.T_i)) / 2
            for base_lr in self.base_lrs
        ]
    
    def step(self, epoch=None):
        if epoch is None:
            epoch = self.last_epoch + 1
            self.T_cur = self.T_cur + 1
            if self.T_cur >= self.T_i:
                self.T_cur = self.T_cur - self.T_i
                self.T_i = self.T_i * self.T_mult
        else:
            if epoch < 0:
                raise ValueError("Expected non-negative epoch, but got {}".format(epoch))
            if epoch >= self.T_0:
                if self.T_mult == 1:
                    self.T_cur = epoch % self.T_0
                else:
                    n = int(math.log((epoch / self.T_0 * (self.T_mult - 1) + 1), self.T_mult))
                    self.T_cur = epoch - self.T_0 * (self.T_mult ** n - 1) / (self.T_mult - 1)
                    self.T_i = self.T_0 * self.T_mult ** n
            else:
                self.T_i = self.T_0
                self.T_cur = epoch
        
        self.last_epoch = math.floor(epoch)
        
        for param_group, lr in zip(self.optimizer.param_groups, self.get_lr()):
            param_group['lr'] = lr

class OneCycleLR(_LRScheduler):
    def __init__(self, optimizer, max_lr, total_steps, pct_start=0.3, anneal_strategy='cos',

View on GitHub (pinned to 40f4cd2652)

Solutions

  1. Pass a non-negative epoch, or call scheduler.step() with no argument to let the scheduler track epochs internally
  2. If computing epoch = step - warmup_steps, clamp it with max(0, step - warmup_steps)
  3. Verify checkpoint-resume logic restores the epoch counter to the correct non-negative value

Example fix

# before
for step, batch in enumerate(loader):
    scheduler.step(step - warmup_steps)  # negative during warmup

# after
for step, batch in enumerate(loader):
    if step >= warmup_steps:
        scheduler.step(step - warmup_steps)
    else:
        scheduler.step()
Defensive patterns

Strategy: validation

Validate before calling

epoch = max(0, global_step - warmup_steps)
scheduler.step(epoch)
# or simply: scheduler.step()  # internal epoch tracking

Try / catch

try:
    scheduler.step(epoch)
except ValueError as e:
    if 'non-negative epoch' in str(e):
        scheduler.step()  # fall back to internal counting
    else:
        raise

Prevention

When it happens

Trigger: Calling scheduler.step(epoch) with epoch=-1, or with a variable that can go negative such as epoch = step_count - warmup_steps before warmup completes, or epoch derived from len(loader) arithmetic that underflows early in training.

Common situations: Custom training loops that pass a raw global step or epoch counter that starts negative (e.g., during a warmup phase), off-by-one errors when computing epochs after resume from checkpoint, or off-by-one when epoch is derived from a zero-based loop index minus an offset.

Related errors


AI-assisted analysis of geekcomputers/Python@40f4cd2652 (2026-08-27). Data as JSON: /api/errors/538ec2276e60505e. Report an issue: GitHub.