pytorch - ✅(Solved) Fix [dynamo] f-strings in Dynamo don't always correctly reflect mutations [2 pull requests, 4 comments, 4 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
pytorch/pytorch#177582Fetched 2026-04-08 00:47:22
View on GitHub
Comments
4
Participants
4
Timeline
53
Reactions
0
Timeline (top)
subscribed ×19mentioned ×18labeled ×9commented ×4

Fix Action

Fixed

PR fix notes

PR #176831: Dynamo update frozen ctor bytecode gen to initialize attributes

Description (problem / solution / changelog)

Fixes #173221

Addresses the bytecode generation order when using FrozenDataClass. Attached gist shows the original bytecode at the top and the bytecode generated after update at the bottom (happens immediately after initialization). https://gist.github.com/trichmo/33b4930b4d3cd91d7ab3f7909aee1ef3

The underlying problem here is that for any FrozenDataClassVariables which were created in the compile region - the object gets initialized without attributes. The attributes are set through the same route as UserDefinedVariable's application of mutations in the bytecode suffix. Because frozen dataclasses have their setattr overriden, an additional set of bytecode to object.setattr must be added at initializtion - this is not a problem for UserDefinedVariables which don't override setattr as their attr is set on init.

This fix has 3 parts:

  1. Update method_setattr_standard to not route through UserDefinedVariable as we don't want to track the attributes as mutations to be applied in the bytecode suffix (no call to tx.output.side_effects.store_attr). Instead, we will directly access the FrozenDataVariable's field when setting it's value.
  2. Add a FrozenDataVariable var_getattr to not use it's superclass's implementation when trying to access fields. Previously the field value would be flagged as has_pending_mutation and tx.output.side_effects.load_attr would get the value stored in side_effects. Because 1 no longer stores as a mutation, we now directly access a field if present and fallback to the super's implementation for other attributes (function handles are tested against).
  3. Update the bytecode generation for the constructor of FrozenDataClassVariable to now not only create the dataclass with uninitialized attributes during the call to codegen_save_tempvars, but also set the attribute value to it's proper initialized state. Same as for the UserDefinedObjectVariable bytecode is generated as "# setattr is defined on this object, so call object.setattr directly", but instead of using the sideeffect tracked value, the field is directly used.

cc @voznesenskym @penguinwu @EikanWang @jgong5 @Guobing-Chen @XiaobingSuper @zhuhaozhe @blzheng @wenzhe-nrv @jiayisunx @kadeng @chauhang @amjames @Lucaskabela @jataylo

Changed files

  • test/dynamo/test_exceptions.py (modified, +19/-0)
  • test/dynamo/test_misc.py (modified, +29/-0)
  • torch/_dynamo/side_effects.py (modified, +18/-1)
  • torch/_dynamo/variables/builder.py (modified, +2/-0)
  • torch/_dynamo/variables/user_defined.py (modified, +20/-2)

Code Example

import torch

class Obj:
    def __init__(self, val):
        self.val = val
    def __repr__(self):
        return f"Obj({self.val})"

@torch.compile(backend="eager")
def fn(x, obj):
    x = x + 1
    s1 = f"obj = {obj}"
    obj.val.append(0)
    s2 = f"obj = {obj}"
    return x, s1, s2

eager_result = fn.__wrapped__(torch.randn(3), Obj([1, 2]))
torch._dynamo.reset()
compiled_result = fn(torch.randn(3), Obj([1, 2]))
print("eager:   ", eager_result[1:])
print("compiled:", compiled_result[1:])
RAW_BUFFERClick to expand / collapse

For example,

import torch

class Obj:
    def __init__(self, val):
        self.val = val
    def __repr__(self):
        return f"Obj({self.val})"

@torch.compile(backend="eager")
def fn(x, obj):
    x = x + 1
    s1 = f"obj = {obj}"
    obj.val.append(0)
    s2 = f"obj = {obj}"
    return x, s1, s2

eager_result = fn.__wrapped__(torch.randn(3), Obj([1, 2]))
torch._dynamo.reset()
compiled_result = fn(torch.randn(3), Obj([1, 2]))
print("eager:   ", eager_result[1:])
print("compiled:", compiled_result[1:])

https://github.com/pytorch/pytorch/pull/176831 also revealed this issue for frozen dataclasses.

According to claude: If arguments to f-strings are not constant, then the f-string is generated in the bytecode, which doesn't properly take mutations into account.

2 approaches to fixing this are:

  1. Constant-fold the f-string at trace time (evaluate __repr__ during tracing and bake the result as a constant), or
  2. Carefully order the bytecode so that f-string evaluations involving frozen dataclasses happen at the correct point relative to mutations of their fields

cc @ezyang @gchanan @kadeng @msaroufim @chauhang @penguinwu @voznesenskym @EikanWang @jgong5 @Guobing-Chen @XiaobingSuper @zhuhaozhe @blzheng @wenzhe-nrv @jiayisunx @amjames @Lucaskabela @jataylo

extent analysis

Fix Plan

To address the issue with f-strings not properly handling mutations, we can implement one of the two proposed approaches. Here, we'll focus on the first approach: constant-folding the f-string at trace time.

Step-by-Step Solution

  1. Evaluate __repr__ during tracing: Modify the fn function to evaluate __repr__ before using it in the f-string.
  2. Bake the result as a constant: Use the evaluated __repr__ result as a constant in the f-string.

Example Code

import torch

class Obj:
    def __init__(self, val):
        self.val = val
    def __repr__(self):
        return f"Obj({self.val})"

@torch.compile(backend="eager")
def fn(x, obj):
    x = x + 1
    obj_repr = repr(obj)  # Evaluate __repr__ during tracing
    s1 = f"obj = {obj_repr}"  # Use the evaluated result as a constant
    obj.val.append(0)
    s2 = f"obj = {repr(obj)}"  # Re-evaluate __repr__ after mutation
    return x, s1, s2

eager_result = fn.__wrapped__(torch.randn(3), Obj([1, 2]))
torch._dynamo.reset()
compiled_result = fn(torch.randn(3), Obj([1, 2]))
print("eager:   ", eager_result[1:])
print("compiled:", compiled_result[1:])

Verification

Run the modified code and verify that the output of eager_result and compiled_result matches, indicating that the f-string is properly handling mutations.

Extra Tips

  • When working with f-strings and mutable objects, consider evaluating the object's representation before using it in the f-string to ensure accurate results.
  • Keep in mind that constant-folding the f-string at trace time may have performance implications, and carefully evaluate the trade-offs for your specific use case.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

pytorch - ✅(Solved) Fix [dynamo] f-strings in Dynamo don't always correctly reflect mutations [2 pull requests, 4 comments, 4 participants]