77from dataclasses import dataclass
88
99from .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
0 commit comments