Skip to content

Commit 4a4e060

Browse files
committed
Add graph break option for Mammut to work around torch.compile issue. Add timm ViT Block discovery if possible.
1 parent e63cef4 commit 4a4e060

5 files changed

Lines changed: 69 additions & 2 deletions

File tree

src/open_clip/mammut_model.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ def __init__(
130130

131131
self.context_length = multimodal_cfg.context_length
132132

133+
# Runtime knob (--torchcompile-pass-break): under full-graph compile, split the graph
134+
# between the two traversals of the shared decoder (contrastive pass, caption pass).
135+
# One graph holding both checkpointed traversals makes the compiled backward retain a
136+
# multi-block recompute working set (2-3.5x eager peak memory, torch 2.9-2.13); the
137+
# break restores eager-like memory at no measured speed cost. Plain attribute rather
138+
# than cfg: it is a compile-time training concern, not part of the model definition.
139+
self.pass_graph_break = False
140+
133141
def set_grad_checkpointing(self, enable: bool = True, impl: str = 'inline'):
134142
self.visual.set_grad_checkpointing(enable, impl=impl)
135143
self.text.set_grad_checkpointing(enable, impl=impl)
@@ -228,6 +236,11 @@ def forward(
228236
if image_latent is None:
229237
return {"text_features": text_latent}
230238

239+
if self.pass_graph_break:
240+
# see ctor note: keep the contrastive and caption decoder traversals in separate
241+
# compiled graphs; a no-op in eager and under the 'blocks' compile strategy
242+
torch._dynamo.graph_break()
243+
231244
# caption pass: causal self-attention w/ cross-attention over projected image tokens
232245
image_kv = image_embs @ self.map_viz2txt_kv
233246

src/open_clip/task/base_task.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,9 @@ def _get_fsdp_shard_modules(self) -> List[Tuple[str, nn.Module]]:
250250
"""Discover modules to shard with FSDP2.
251251
252252
Default: finds all ResidualAttentionBlock, CustomResidualAttentionBlock,
253-
ModernTextBlock, and Bottleneck instances within the trainable module.
253+
ModernTextBlock, Bottleneck, and timm ViT ``Block`` (timm towers, incl. NaFlexVit
254+
trunks) instances within the trainable module. Also drives the 'blocks' compile
255+
strategy, so timm vision trunks get per-block compile rather than staying eager.
254256
Models can override this by defining a ``fsdp_shard_modules()`` method.
255257
"""
256258
model = unwrap_model(self.trainable_module)
@@ -260,7 +262,13 @@ def _get_fsdp_shard_modules(self) -> List[Tuple[str, nn.Module]]:
260262
from open_clip.transformer import ResidualAttentionBlock, CustomResidualAttentionBlock, ModernTextBlock
261263
from open_clip.modified_resnet import Bottleneck
262264

263-
shard_types = (ResidualAttentionBlock, CustomResidualAttentionBlock, ModernTextBlock, Bottleneck)
265+
shard_types = [ResidualAttentionBlock, CustomResidualAttentionBlock, ModernTextBlock, Bottleneck]
266+
try:
267+
from timm.models.vision_transformer import Block as TimmViTBlock
268+
shard_types.append(TimmViTBlock)
269+
except ImportError:
270+
pass
271+
shard_types = tuple(shard_types)
264272

265273
modules = []
266274
for name, mod in model.named_modules():

src/open_clip_train/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,15 @@ def main(args):
412412
_logger.info(f'Disabling DDP dynamo optimizer ({reason}).')
413413
torch._dynamo.config.optimize_ddp = False
414414

415+
if args.torchcompile and args.torchcompile_pass_break:
416+
inner = unwrap_model(model)
417+
if hasattr(inner, 'pass_graph_break'):
418+
inner.pass_graph_break = True
419+
_logger.info('Enabling graph break between contrastive and caption decoder passes.')
420+
else:
421+
_logger.warning(
422+
'--torchcompile-pass-break: model has no dual-pass graph break support; ignoring.')
423+
415424
if args.torchcompile and args.torchcompile_strategy in ('model', 'blocks'):
416425
if args.fsdp:
417426
_logger.info(

src/open_clip_train/params.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,17 @@ def parse_args(args):
555555
"automatic dynamic otherwise switches to a symbolic graph on the second shape, "
556556
"with worse memory planning.",
557557
)
558+
parser.add_argument(
559+
"--torchcompile-pass-break",
560+
default=False,
561+
action='store_true',
562+
help="Insert a dynamo graph break between the contrastive and caption passes of dual-pass "
563+
"decoder models (MaMMUT) under full-graph compile. One graph holding two checkpointed "
564+
"traversals of the same decoder blocks makes the compiled backward retain a multi-block "
565+
"recompute working set (2-3.5x eager peak memory); breaking between the passes restores "
566+
"eager-like memory at no measured speed cost. Not needed with "
567+
"--torchcompile-strategy blocks (recompute already stays eager there).",
568+
)
558569
parser.add_argument(
559570
"--accum-freq", type=int, default=1, help="Update the model every --acum-freq steps."
560571
)

tests/test_naflex_mammut.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,29 @@ def test_registered_config_builds(name, patch, seq_len):
281281
assert model.visual.trunk.get_patch_size() == (patch, patch)
282282
assert not isinstance(model.visual.trunk.norm_pre, torch.nn.Identity)
283283
assert model.visual.output_tokens
284+
285+
286+
# ---------------------------------------------------------------- task integration extras
287+
288+
def test_blocks_discovery_includes_timm_trunk():
289+
"""The FSDP/blocks-compile discovery must cover the timm vision trunk, not just the text
290+
stacks -- otherwise --torchcompile-strategy blocks leaves the whole vision tower eager."""
291+
model = _tiny_model().train()
292+
task = CoCaTask(model, verbose=False)
293+
names = [n for n, _ in task._get_fsdp_shard_modules()]
294+
trunk = [n for n in names if n.startswith('visual.trunk.blocks')]
295+
assert len(trunk) == TINY_VISION_CFG['timm_model_kwargs']['depth'], \
296+
f'timm trunk blocks not discovered: {names}'
297+
298+
299+
def test_mammut_pass_graph_break_eager_noop():
300+
"""pass_graph_break must be a pure compile-time hint: eager outputs identical either way."""
301+
model = _tiny_model()
302+
batch, text = _patch_batch(), _text_batch()
303+
with torch.no_grad():
304+
base = model(image=batch, text=text)
305+
model.pass_graph_break = True
306+
split = model(image=batch, text=text)
307+
for k, v in base.items():
308+
if torch.is_tensor(v):
309+
torch.testing.assert_close(v, split[k], rtol=0, atol=0)

0 commit comments

Comments
 (0)