Skip to content

Make the gradient-accumulation rewrite emit group references - #10818

Open
vineethsaivs wants to merge 2 commits into
unslothai:mainfrom
vineethsaivs:fix/grad-accum-rewrite-backrefs
Open

Make the gradient-accumulation rewrite emit group references#10818
vineethsaivs wants to merge 2 commits into
unslothai:mainfrom
vineethsaivs:fix/grad-accum-rewrite-backrefs

Conversation

@vineethsaivs

Copy link
Copy Markdown
Contributor

What breaks

patch_gradient_accumulation_fix rewrites training_step so accelerator.backward runs on the divided loss instead of the full one. The regex is raw. The replacement is not:

"else:\n"
"\2if num_items_in_batch is None:\n"
"\3loss = loss / self.args.gradient_accumulation_steps\n"
"\1self.accelerator.backward(loss, **kwargs)",

"\1", "\2" and "\3" are octal escapes. Python turns them into 0x01, 0x02 and 0x03 long before re.sub gets a chance to read them as group references, so the three captured indents come back as control characters:

'else:\n\x02if num_items_in_batch is None:\n\x03loss = loss / self.args.gradient_accumulation_steps\n\x01self.accelerator.backward(loss, **kwargs)'

The exec(function, globals()) two lines below then raises SyntaxError: unexpected indent rather than installing the patched training_step.

What changed

A raw template with \g<1>, \g<2>, \g<3>. The rewrite now produces what it was written to produce:

    else:
        if num_items_in_batch is None:
            loss = loss / self.args.gradient_accumulation_steps
        self.accelerator.backward(loss, **kwargs)
    return loss.detach()

How live is it

Not live. The shape this matches is what training_step looked like before huggingface/transformers#35808 reordered the two. I checked the whole pinned range in pyproject.toml (transformers>=4.51.3,<=5.5.0):

version pattern matches
4.51.3 (minimum pin) no
4.56.0 no
5.5.0 (maximum pin) no

So today the branch is a silent no-op, and this is a landmine rather than a crash anyone is hitting. Deleting the block is a fair call instead and I am happy to send that version if you prefer it. I did not assume it, because the other three replacements in the same function are also inert on these versions and I did not want to decide that for you.

Tests

tests/test_gradient_accumulation_rewrite.py reads the pattern and the replacement out of _utils.py with ast and runs them against the old training_step shape, since _utils.py cannot be imported without an accelerator.

  • the rewrite moves backward below the division
  • the result carries no control characters, parses, and keeps backward one indent level outside the if
2 passed in 0.20s

The second fails on main:

assert not any(character in rewritten for character in "\x01\x02\x03")
E  assert not True

patch_gradient_accumulation_fix moves accelerator.backward below the division so
the gradients carry the divided loss. Its regex is raw but its replacement is
not, so "\1", "\2" and "\3" are octal escapes that Python turns into 0x01, 0x02
and 0x03 long before re.sub can read them as group references.

The three captured indents therefore came back as control characters:

    'else:\n\x02if num_items_in_batch is None:\n\x03loss = loss / ...'

and the exec two lines down raises SyntaxError instead of installing the patched
training_step.

Use a raw template with \g<1>, \g<2> and \g<3>. The rewrite then produces what it
always meant to.

Not reachable today: the shape it matches predates
huggingface/transformers#35808, and 4.51.3, 4.56.0 and 5.5.0 all fail the match,
which is the whole pinned range. So this is a landmine rather than a live crash,
and deleting the block instead is a fair call. Say the word and I will.

Test: tests/test_gradient_accumulation_rewrite.py reads the pattern and the
replacement out of _utils.py with ast and runs them against the old
training_step. One test pins the ordering, the other that the result carries no
control characters, parses, and keeps backward one indent level outside the
division. The second fails on main.
@danielhanchen

Copy link
Copy Markdown
Member

Confirmed the replacement in unsloth/models/_utils.py still uses plain backslash-digit strings, so the captured indents come back as control bytes if that pattern ever matches again. Will get this reviewed, and I may take you up on the delete-the-block variant instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants