Skip to content

Commit 342acb7

Browse files
committed
Add NaFlexViT support to CoCa2 and train a cc12m test model
1 parent 2b446e3 commit 342acb7

6 files changed

Lines changed: 393 additions & 23 deletions

File tree

src/open_clip/coca_model.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dataclasses import dataclass
88

99
from .transformer import (
10+
AttentionalPooler,
1011
LayerNormFp32,
1112
LayerNorm,
1213
QuickGELU,
@@ -83,10 +84,32 @@ def __init__(
8384
text_cfg = CLIPTextCfg(**text_cfg) if isinstance(text_cfg, dict) else text_cfg
8485
vision_cfg = CLIPVisionCfg(**vision_cfg) if isinstance(vision_cfg, dict) else vision_cfg
8586
if vision_cfg.timm_model_name:
87+
# timm towers are supported in token mode only, with model-level attentional pooling:
88+
# the tower returns {'pooled', 'patch_tokens', 'patch_valid'} and the paper poolers
89+
# (generative n_queries + contrastive) live on the model, pooling the trunk tokens with
90+
# validity masking. Masking terminates at the pooler: the caption decoder cross-attends
91+
# the fixed-count pooled queries, so no context_valid threading is needed downstream
92+
# (contrast MaMMUT, which cross-attends raw patch tokens and threads image_embs_valid).
93+
if not vision_cfg.output_tokens:
94+
raise ValueError(
95+
"CoCa with a timm vision tower requires vision_cfg.output_tokens=true "
96+
"(token mode) so model-level attentional pooling can consume trunk tokens.")
97+
if vision_cfg.attentional_pool not in ('cascade', 'parallel'):
98+
raise ValueError(
99+
"CoCa with a timm vision tower requires vision_cfg.attentional_pool "
100+
"'cascade' or 'parallel' (paper pooling); the caption decoder consumes the "
101+
"pooled queries, not raw patch tokens.")
102+
if getattr(text_cfg, 'text_arch', None) == 'modern' and \
103+
getattr(text_cfg, 'attention_mode', None) == 'bidirectional':
104+
# The caption decoder consumes the tower's contextualized token embeddings; under
105+
# bidirectional attention position i carries future-token information the decoder's
106+
# causal mask cannot undo, so the caption loss can copy and generation would mismatch
107+
# training. Fail fast rather than train a silently-broken caption objective. (Masked
108+
# 'mean' pooling for the contrastive readout works fine over a causal tower.)
86109
raise ValueError(
87-
"CoCa does not support timm vision towers: caption cross-attention requires token projection "
88-
"and validity handling that only MaMMUT's timm path currently provides."
89-
)
110+
"CoCa does not support a bidirectional modern text tower "
111+
"(text_cfg.attention_mode='bidirectional'): its token embeddings leak future "
112+
"tokens into the caption decoder. Use attention_mode='causal'.")
90113

91114
self.text = _build_text_tower(
92115
embed_dim=embed_dim,
@@ -108,6 +131,40 @@ def __init__(
108131
cast_dtype=cast_dtype,
109132
)
110133

134+
if vision_cfg.timm_model_name:
135+
# model-level paper poolers over trunk tokens (see the token-mode note above)
136+
norm_layer = LayerNormFp32 if cast_dtype in (torch.float16, torch.bfloat16) else LayerNorm
137+
trunk_dim = self.visual.trunk.num_features
138+
self.attn_pool_type = vision_cfg.attentional_pool
139+
self.pool_norm = norm_layer(trunk_dim) # pre-pool norm, mirrors native ln_post-on-width
140+
self.attn_pool = AttentionalPooler(
141+
embed_dim,
142+
trunk_dim,
143+
n_head=vision_cfg.attn_pooler_heads,
144+
n_queries=vision_cfg.attn_pooler_queries,
145+
)
146+
self.attn_pool_contrastive = AttentionalPooler(
147+
embed_dim,
148+
embed_dim if self.attn_pool_type == 'cascade' else trunk_dim,
149+
n_head=vision_cfg.attn_pooler_heads,
150+
n_queries=1,
151+
)
152+
with torch.no_grad(): # match VisionTransformer.init_parameters pooler query init
153+
nn.init.normal_(self.attn_pool.query, std=embed_dim ** -0.5)
154+
nn.init.normal_(self.attn_pool_contrastive.query, std=embed_dim ** -0.5)
155+
# Paper pooling replaces the tower's pooled readout entirely; its parameters
156+
# (trunk fc_norm + TimmModel head proj) would never receive grad and trip DDP's
157+
# unused-parameter check. Remove them rather than carry dead weights. (MaMMUT keeps
158+
# them: it uses the tower's pooled output as the contrastive latent.)
159+
self.visual.head = nn.Identity()
160+
if getattr(self.visual.trunk, 'fc_norm', None) is not None:
161+
self.visual.trunk.fc_norm = nn.Identity()
162+
else:
163+
self.attn_pool_type = ''
164+
self.pool_norm = None
165+
self.attn_pool = None
166+
self.attn_pool_contrastive = None
167+
111168
self.text_decoder = _build_text_decoder_tower(
112169
vocab_size,
113170
multimodal_cfg=multimodal_cfg,
@@ -140,7 +197,20 @@ def set_grad_checkpointing(self, enable: bool = True, impl: str = 'inline'):
140197
self.text_decoder.set_grad_checkpointing(enable, impl=impl)
141198

142199
def _encode_image(self, images, normalize: bool = True):
143-
image_latent, tokens_embs = self.visual(images)
200+
out = self.visual(images)
201+
if isinstance(out, dict):
202+
# timm token mode: {'pooled', 'patch_tokens', 'patch_valid'} -- masked model-level
203+
# pooling; the tower's own 'pooled' readout is unused (paper pooling replaces it).
204+
tokens = self.pool_norm(out['patch_tokens'])
205+
patch_valid = out['patch_valid']
206+
tokens_embs = self.attn_pool(tokens, key_valid=patch_valid)
207+
if self.attn_pool_type == 'cascade':
208+
# pooled queries are fixed-count and all valid -> no mask from here on
209+
image_latent = self.attn_pool_contrastive(tokens_embs)[:, 0]
210+
else: # parallel: contrastive pooler reads the (masked) trunk tokens directly
211+
image_latent = self.attn_pool_contrastive(tokens, key_valid=patch_valid)[:, 0]
212+
else:
213+
image_latent, tokens_embs = out
144214
image_latent = F.normalize(image_latent, dim=-1) if normalize else image_latent
145215
return image_latent, tokens_embs
146216

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"embed_dim": 512,
3+
"vision_cfg": {
4+
"timm_model_name": "naflexvit_base_patch16_gap",
5+
"timm_model_kwargs": {"patch_size": 32, "pre_norm": true, "pos_embed_grid_size": [8, 8]},
6+
"timm_pool": "avg",
7+
"timm_proj": "linear",
8+
"output_tokens": true,
9+
"image_size": 256,
10+
"image_seq_len": 64,
11+
"attentional_pool": "parallel",
12+
"attn_pooler_queries": 128,
13+
"attn_pooler_heads": 8
14+
},
15+
"text_cfg": {
16+
"text_arch": "modern",
17+
"context_length": 128,
18+
"variable_text": true,
19+
"vocab_size": 50260,
20+
"pad_id": 50258,
21+
"bos_id": 50259,
22+
"eos_id": 50257,
23+
"width": 512,
24+
"heads": 8,
25+
"layers": 12,
26+
"pool_type": "mean",
27+
"attention_mode": "causal",
28+
"pos_embed": "rope",
29+
"mlp_type": "swiglu",
30+
"norm_type": "rmsnorm",
31+
"norm_eps": 1e-06,
32+
"qk_norm": true,
33+
"attn_gated": true,
34+
"output_tokens": true,
35+
"tokenizer_type": "tiktoken",
36+
"tiktoken_name": "r50k_base",
37+
"tokenizer_kwargs": {
38+
"clean": "whitespace"
39+
}
40+
},
41+
"multimodal_cfg": {
42+
"text_arch": "modern",
43+
"context_length": 128,
44+
"vocab_size": 50260,
45+
"pad_id": 50258,
46+
"bos_id": 50259,
47+
"eos_id": 50257,
48+
"width": 512,
49+
"heads": 8,
50+
"layers": 12,
51+
"pos_embed": "rope",
52+
"mlp_type": "swiglu",
53+
"norm_type": "rmsnorm",
54+
"norm_eps": 1e-06,
55+
"qk_norm": true,
56+
"attn_gated": true
57+
},
58+
"custom_text": true
59+
}

src/open_clip/transformer.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,16 @@ def __init__(
264264
self.ln_q = norm_layer(d_model)
265265
self.ln_k = norm_layer(context_dim)
266266

267-
def forward(self, x: torch.Tensor):
267+
def forward(self, x: torch.Tensor, key_valid: Optional[torch.Tensor] = None):
268+
"""key_valid: optional [B, L] bool/int validity (True/1 = real K/V token). Padded keys are
269+
excluded from pooling attention -- required for NaFlex/variable-length token inputs where
270+
trailing positions are padding, not content."""
268271
N = x.shape[0]
269272
x = self.ln_k(x)
270273
q = self.ln_q(self.query)
271-
out = self.attn(q.unsqueeze(0).expand(N, -1, -1), k_x=x, v_x=x)
274+
# [B, 1, 1, L] bool key mask, broadcast over heads/queries (incl. all-invalid-row guard)
275+
attn_mask = context_attn_mask_from_valid(key_valid)
276+
out = self.attn(q.unsqueeze(0).expand(N, -1, -1), k_x=x, v_x=x, attn_mask=attn_mask)
272277
return out
273278

274279

tests/test_mammut.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,10 @@ def test_mammut_modern_factory_create():
460460
model = open_clip.create_model('mammut2-moderntext_ViT-B-32')
461461
assert isinstance(model, MaMMUT)
462462
assert isinstance(model.text, ModernMultimodalDecoder)
463+
# config switched to tiktoken r50k / ctx 128 / qk-norm in f326982
463464
tokenizer = open_clip.get_tokenizer('mammut2-moderntext_ViT-B-32')
464-
assert tokenizer.context_length == 77
465+
assert tokenizer.context_length == 128
466+
assert model.text.token_embedding.weight.shape[0] == 50260
465467

466468

467469
def test_mammut_modern_cross_sublayer_placement():

0 commit comments

Comments
 (0)