dmis-lab commited on
Commit
40600d5
·
verified ·
1 Parent(s): a381a62

Update config and release files

Browse files
README.md CHANGED
@@ -88,12 +88,16 @@ inputs = model.prepare_inputs(
88
  image,
89
  "What is the dominant color? A. Red B. Blue C. Green D. Yellow. Answer with the option letter.",
90
  )
91
- logits = model.next_token_logits(**inputs)
92
- token_id = logits.argmax(dim=-1).item()
93
- print(model.tokenizer.decode([token_id]))
 
 
 
 
94
  ```
95
 
96
- This model exposes the strict first-answer-token readout used in our analyses. Multi-token generation is not currently supported by this loader.
97
 
98
  Use one complete model replica per GPU. This custom Transformers loader does
99
  not support automatic model sharding, generic `save_pretrained()` reserialization,
 
88
  image,
89
  "What is the dominant color? A. Red B. Blue C. Green D. Yellow. Answer with the option letter.",
90
  )
91
+ answer_ids = model.generate(
92
+ **inputs,
93
+ do_sample=False,
94
+ max_new_tokens=64,
95
+ eos_token_id=model.tokenizer.eos_token_id,
96
+ )
97
+ print(model.tokenizer.decode(answer_ids[0], skip_special_tokens=True))
98
  ```
99
 
100
+ This model supports strict first-answer-token readout and deterministic greedy answer generation. The default recurrent update coefficient is `beta=0.5`.
101
 
102
  Use one complete model replica per GPU. This custom Transformers loader does
103
  not support automatic model sharding, generic `save_pretrained()` reserialization,
config.json CHANGED
@@ -15,16 +15,7 @@
15
  "recurrent_layer": 36,
16
  "upper_decoder_start": 37,
17
  "inference_T": 4,
18
- "inference_beta": 0.33,
19
- "lora_rank": 32,
20
- "lora_alpha": 12.0,
21
- "lora_dropout": 0.01,
22
- "checkpoint_step": 500,
23
- "format": "cvrr_native_plus_merged_transition_v1",
24
- "merged_weight_dtype": "float32",
25
- "native_weight_bytes": 18277586944,
26
- "status": "gpu_vstar_comparison_completed",
27
- "upload_ready": true,
28
- "hub_model_id": "dmis-lab/InternVL3-9B-CVRR"
29
  }
30
  }
 
15
  "recurrent_layer": 36,
16
  "upper_decoder_start": 37,
17
  "inference_T": 4,
18
+ "inference_beta": 0.5,
19
+ "format": "cvrr_native_plus_merged_transition_v1"
 
 
 
 
 
 
 
 
 
20
  }
21
  }
configuration_source_qwen25.py DELETED
@@ -1,1142 +0,0 @@
1
- """Configuration for CLOSE latent reasoning on Qwen2.5-VL.
2
-
3
- The current ``raw_evidence_question`` path keeps the complete layer-``ell_star``
4
- visual memory as immutable evidence and recurrently updates the text-only
5
- question state by cross-attending to it. Its selected interface averages all
6
- four residual recurrent states and may use a training-only restoration target
7
- from the normal multimodal question state. Historical slot-workspace fields are
8
- retained so old checkpoints remain loadable, but they are mutually exclusive
9
- with the raw-evidence path.
10
-
11
- Every architectural hyperparameter of Method §3 lives here -- nothing is
12
- hardcoded in ``modeling_close_qwen2_5_vl.py``.
13
-
14
- Layer-index convention (used consistently across the repo)
15
- ----------------------------------------------------------
16
- ``ell_star`` is **0-indexed and inclusive**: it is the index of the last decoder
17
- layer belonging to the lower branch. With ``num_hidden_layers == 28``::
18
-
19
- F_{<=l*} = language_model.layers[: ell_star + 1] # lower_slice
20
- F_{>l*} = language_model.layers[ell_star + 1 :] # upper_slice
21
-
22
- so ``ell_star=18`` means layers 0..18 run before the split and 19..27 after it.
23
- ``ell_star`` is measured once by ``scripts/localize_read_point.py`` (§3.1) and
24
- frozen before any workspace training.
25
-
26
- Careful with ``Qwen2_5_VLConfig``: its ``__setattr__`` forwards any attribute
27
- whose name already exists in ``text_config.__dict__`` down to the sub-config, and
28
- its ``__init__`` builds ``text_config`` from ``**kwargs``. Workspace fields are
29
- therefore declared as explicit named parameters so they never enter ``kwargs``
30
- and never shadow a text-config field. ``tests/test_config_roundtrip.py`` guards
31
- this.
32
- """
33
-
34
- from __future__ import annotations
35
-
36
- import math
37
-
38
- from transformers.models.qwen2_5_vl.configuration_qwen2_5_vl import Qwen2_5_VLConfig
39
-
40
- #: How the ``S`` workspace slots are assigned M-RoPE positions once they are
41
- #: spliced into the replacement cache above ``ell_star``. Qwen2.5-VL uses 3D
42
- #: M-RoPE (t, h, w) with ``mrope_section=[16, 24, 24]``, so slots -- which have
43
- #: no spatial extent -- need an explicit convention.
44
- WORKSPACE_ROPE_MODES = (
45
- # Slots continue the 1-D text position sequence directly after Q*, with
46
- # t == h == w. Treats the workspace as "more text".
47
- "continue",
48
- # Slots reuse the (t, h, w) span the image tokens occupied in the original
49
- # multimodal prefill, subsampled to S positions. Preserves the read point's
50
- # positional geometry but reintroduces image-derived indices.
51
- "image_span",
52
- # All slots share one position (the first position after Q*). Makes the
53
- # workspace order-free, matching the set semantics of Perceiver-style slots.
54
- "shared",
55
- )
56
-
57
-
58
- class CloseQwen2_5_VLConfig(Qwen2_5_VLConfig):
59
- r"""Configuration for :class:`CloseQwen2_5_VLForConditionalGeneration`.
60
-
61
- Args:
62
- ell_star: 0-indexed inclusive last layer of the lower branch. ``None``
63
- until §3.1 localization has run; the model refuses to build with
64
- ``None`` rather than falling back to a guessed depth.
65
- num_workspace_steps: the fixed recurrent horizon (paper notation ``T``;
66
- per-item segment counts ``n`` exist only in the losses, never here).
67
- The trainable parameter count is independent of it because
68
- ``f_theta`` is shared across steps, so it is a free experimental
69
- axis. ``0`` is legal and is the no-recurrence floor: ``Z^(0)`` goes
70
- straight to the decoder.
71
- num_workspace_slots: ``S``, slots in ``Z``. Free capacity knob.
72
- workspace_width: ``d_w``, the slot width. Free capacity knob -- the
73
- method's claim is path exclusivity (all visual information routed
74
- through ``Z``), NOT an information bottleneck, so ``S`` and ``d_w``
75
- carry no methodological commitment. Kept below ``d`` (3584 for the
76
- 7B) since ``P_Z`` projects up at the splice.
77
- workspace_num_heads: attention heads inside ``r_theta`` / ``f_theta``.
78
- read_num_blocks: cross-attention blocks in ``r_theta``. The *read* is
79
- once regardless of depth: all blocks see the same ``V*`` in a single
80
- forward, and ``V*`` is discarded afterwards.
81
- transition_num_blocks: blocks in the shared recurrent ``f_theta``.
82
- workspace_ffn_mult: FFN expansion inside workspace blocks.
83
- workspace_dropout: dropout inside workspace blocks.
84
- workspace_rope_mode: one of :data:`WORKSPACE_ROPE_MODES`.
85
- adapter_rank: rank of the low-rank adapter on the post-``ell_star``
86
- backbone layers. ``0`` disables it (frozen backbone only).
87
- adapter_alpha: LoRA-style scaling; effective scale is
88
- ``adapter_alpha / adapter_rank``.
89
- adapter_dropout: dropout probability applied only to the LoRA branch.
90
- lambda_traj: legacy trajectory-loss weight. The current method sets it
91
- to zero and trains with final-answer CE only; it remains serialized
92
- solely so historical checkpoints can still be inspected.
93
- """
94
-
95
- model_type = "close_qwen2_5_vl"
96
-
97
- def __init__(
98
- self,
99
- ell_star: int | None = None,
100
- num_workspace_steps: int = 4,
101
- num_workspace_slots: int = 32,
102
- workspace_width: int = 512,
103
- workspace_num_heads: int = 8,
104
- read_num_blocks: int = 1,
105
- transition_num_blocks: int = 2,
106
- workspace_ffn_mult: int = 4,
107
- workspace_dropout: float = 0.0,
108
- workspace_rope_mode: str = "continue",
109
- adapter_rank: int = 16,
110
- adapter_alpha: int = 32,
111
- adapter_dropout: float = 0.0,
112
- adapter_exclude_layers: tuple = (),
113
- lambda_traj: float = 0.0,
114
- read_shortcut: bool = False,
115
- splice_scale_match: bool = False,
116
- read_gating: bool = False,
117
- gate_init_eps: float = 0.02,
118
- read_residual_pure: bool = False,
119
- residual_norm_cap: float = 0.0,
120
- pure_warm_start: bool = False,
121
- final_verify: bool = False,
122
- mid_read_step: int = 0,
123
- persistent_slot_id: bool = False,
124
- mid_read_alpha: float = 1.0,
125
- evidence_reasoning: bool = False,
126
- raw_evidence_question: bool = False,
127
- raw_attention_source_layer: int = -1,
128
- raw_transition_ffn: bool | None = None,
129
- raw_state_replacement: bool = False,
130
- raw_question_anchor: bool = False,
131
- raw_mean_aggregation: bool = False,
132
- hierarchical_visual_layers: tuple = (),
133
- counterfactual_residual_recurrence: bool = False,
134
- visual_counterfactual_recurrence: bool = False,
135
- visual_cumulative_recurrence: bool = False,
136
- visual_full_state_recurrence: bool = False,
137
- visual_boundary_reentry: bool = False,
138
- visual_source_centered_reentry: bool = False,
139
- recurrence_only_adapter: bool = False,
140
- visual_preserving_adapter_correction: bool = False,
141
- perceive_deliberate_chain: bool = False,
142
- local_visual_latent_chain: bool = False,
143
- pdlc_transition_conditioned: bool = False,
144
- pdlc_question_visible_through_pair: int = 1,
145
- pdlc_question_curriculum: bool = False,
146
- latent_chain_init_token_id: int = -1,
147
- spatial_visual_recurrence: bool = False,
148
- spatial_recurrence_steps: int = 8,
149
- spatial_recurrence_inner_width: int = 256,
150
- spatial_recurrence_heads: int = 8,
151
- spatial_step_override: int = -1,
152
- counterfactual_beta: float = 1.0,
153
- crr_concat_aggregation: bool = False,
154
- crr_policy_rank: int = 0,
155
- crr_policy_alpha: float = 32.0,
156
- counterfactual_reliance_loss: bool = False,
157
- transition_aware_reliance_loss: bool = False,
158
- corrective_transition_reliance_loss: bool = False,
159
- counterfactual_step_retention_loss: bool = False,
160
- functional_state_loss: bool = False,
161
- per_example_answer_loss: bool = False,
162
- interface_loss: bool = False,
163
- read_anchor: bool = False,
164
- **kwargs,
165
- ):
166
- super().__init__(**kwargs)
167
- self.ell_star = ell_star
168
- self.num_workspace_steps = num_workspace_steps
169
- self.num_workspace_slots = num_workspace_slots
170
- self.workspace_width = workspace_width
171
- self.workspace_num_heads = workspace_num_heads
172
- self.read_num_blocks = read_num_blocks
173
- self.transition_num_blocks = transition_num_blocks
174
- self.workspace_ffn_mult = workspace_ffn_mult
175
- self.workspace_dropout = workspace_dropout
176
- self.workspace_rope_mode = workspace_rope_mode
177
- self.adapter_rank = adapter_rank
178
- self.adapter_alpha = adapter_alpha
179
- self.adapter_dropout = adapter_dropout
180
- # E run: dense full-FT 층은 LoRA 대상에서 제외 (이중 파라미터화 방지)
181
- self.adapter_exclude_layers = list(adapter_exclude_layers or ())
182
- self.lambda_traj = lambda_traj
183
- # v2 anti-collapse switches (defaults False so v1 checkpoints reload).
184
- # read_shortcut: r_theta adds an orthogonally-projected pooled-V* term to
185
- # every slot, so Z^(0) is image-dependent from step 0 instead of relying
186
- # on a random cross-attention finding signal.
187
- # splice_scale_match: P_Z(Z) rows are RMS-matched to the question rows at
188
- # the splice. Unmatched, slot states are ~O(1) against layer-20 hiddens
189
- # in the hundreds -- the decoder could ignore them for free, and did
190
- # (probe 20143: visual sensitivity 0.94 -> 1.0000 over training).
191
- self.read_shortcut = read_shortcut
192
- self.splice_scale_match = splice_scale_match
193
- # v6 candidate: gated re-reads DURING the recurrence. Every re-read
194
- # goes through r_theta into Z, so the mediation invariant (decoder sees
195
- # only [Q*; P_Z(Z)]) is untouched; what changes is that the workspace
196
- # may consult V* again mid-reasoning, scaled by a learned scalar gate.
197
- # The gate is free at EVERY step -- no hand-coded schedule. Degeneration
198
- # into answer-time retrieval is disciplined by the objective itself:
199
- # L_traj pins each intermediate state to its teacher segment, so a
200
- # wait-then-fetch trajectory misses its intermediate targets. The gate
201
- # bias initialises negative, i.e. training starts from the proven
202
- # no-reread regime and departs only where gradients demand it.
203
- # Default False: v4/v5 checkpoints reload bit-identically.
204
- self.read_gating = read_gating
205
- # Initial gate open-rate epsilon_gamma: b_gamma = logit(eps). Sigmoid
206
- # never reaches exactly 0 at finite logits, so this is near-identity
207
- # (not bitwise) initialisation w.r.t. the read-once path.
208
- self.gate_init_eps = gate_init_eps
209
- # v7: always-on PURE visual residual rereads. Every step adds
210
- # Z = U + W_o XAttn(LN(U) -> queries, V* -> keys/values): no self-attn,
211
- # no FFN, no output norm, no biases in the branch, W_o zero-init.
212
- # Motivation (battery, ep1 ckpt 20752): read_once hurt badly while
213
- # null_step (blank memory at one step) was harmless -- the gated
214
- # reader's value was extra recurrent DEPTH, not vision. This branch
215
- # removes every path that can change Z without V* content, so any
216
- # benefit it shows IS visual by construction.
217
- self.read_residual_pure = read_residual_pure
218
- # v7.1 (advisor): relative-norm trust region on the visual residual.
219
- # s_k = min(1, rho * RMS(U) / (RMS(Delta) + eps)); Z = U + s_k * Delta
220
- # Unlike a fixed alpha, the optimizer cannot undo this by rescaling
221
- # W_o (s_k adapts); ||Delta_used|| <~ rho * ||U|| structurally, and
222
- # V* = 0 => Delta = 0 keeps the zero-memory identity. 0.0 disables.
223
- self.residual_norm_cap = residual_norm_cap
224
- # v7.1: initialise pure_attn K/V projections from the initial visual
225
- # reader's v_in (same [d_w, d] shape) and Q from its first read
226
- # block's in-proj slice, instead of random xavier -- the ep1 finding
227
- # was that Q/K/V stayed ~random while only W_o trained.
228
- self.pure_warm_start = pure_warm_start
229
- # MAIN METHOD (advisor-confirmed): Final-Verified Recurrent Workspace.
230
- # Initial read -> PURE latent recurrence (no mid-step visual anything)
231
- # -> exactly ONE pure visual verification at the end:
232
- # Z_out = U^(T) + W_o XAttn(LN(U^(T)), V*), alpha = 1, W_o = 0 init.
233
- # Uses the same bias-free verifier module as read_residual_pure; the
234
- # two flags differ only in WHERE the branch fires (every step vs once).
235
- self.final_verify = final_verify
236
- # MAIN (advisor 최종 확정): Mid-Read Recurrent Workspace. 고정 상수
237
- # k* = floor(T/2) = 4 (스텝 sweep 산물이 아니라 판독 전후 동수 전이의
238
- # midpoint)에서 pure visual read 1회:
239
- # Z^(4) = U^(4) + W_o XAttn(LN(U^(4)), V*), alpha=1, W_o=0 init.
240
- # 논리적 step 4 == python 루프 index 3 (0-based) -- off-by-one은
241
- # tests/test_mid_read.py가 상태 분기 위치로 고정한다. 0이면 비활성.
242
- self.mid_read_step = mid_read_step
243
- # advisor(2026-08-06): E_slot(=workspace_read.slots)을 매 recurrent
244
- # step 입력과 mid-read query에 일시 제공. state에는 누적하지 않는다.
245
- self.persistent_slot_id = persistent_slot_id
246
- # 고정 read 스케일 Z = U + alpha*W_o A (advisor scale-mismatch 검사).
247
- # 1.0 = 기존과 비트동일. eval의 gate_overrides alpha는 절대값으로 이를 대체.
248
- self.mid_read_alpha = mid_read_alpha
249
- # advisor 최종안: E(persistent evidence) / R(recurrent reasoning) 분리.
250
- # states = [E^(0), R^(1..T)], 결합 Z^(T)=E+R^(T)는 prefill에서.
251
- self.evidence_reasoning = evidence_reasoning
252
- # Current replacement candidate: no learned evidence/reasoning slots.
253
- # E is the complete raw V* sequence and R0 is the text-only Q* sequence.
254
- # A single shared CrossAttn transition produces R1..RT. Residual mode
255
- # decodes RT; the replacement candidate decodes Q*+HT. Cross-attention
256
- # uses the backbone's native GQA shape and
257
- # is initialized from the first upper layer by default. Checkpoints
258
- # created before the CrossAttn-only decision did not serialize
259
- # raw_transition_ffn; None therefore preserves their historical FFN
260
- # for faithful evaluation, while every new training launch passes False.
261
- self.raw_evidence_question = bool(raw_evidence_question)
262
- self.raw_attention_source_layer = int(raw_attention_source_layer)
263
- self.raw_transition_ffn = (
264
- True if raw_transition_ffn is None else bool(raw_transition_ffn)
265
- )
266
- # Experimental simplification of the same raw-E/Q path. The residual
267
- # carrier is replaced, not augmented:
268
- # H0=Q*, Hk=CrossAttn(H{k-1}, E), decoder_latent=Q*+HT.
269
- # False preserves all residual raw-E/Q checkpoints and active runs.
270
- self.raw_state_replacement = bool(raw_state_replacement)
271
- # Question-anchored replacement keeps the fixed text scaffold in every
272
- # recurrent query without restoring the old recurrent identity path:
273
- # Z0=Q*, Zk=Q*+CrossAttn(Z{k-1}, E), decoder_latent=ZT.
274
- # This replaces the one-time Q*+HT decoder interface above.
275
- self.raw_question_anchor = bool(raw_question_anchor)
276
- # Q-shaped recurrent candidate selected by the V* interface oracle:
277
- # R0=Q*, Rk=R{k-1}+CrossAttn(R{k-1}, V*)
278
- # R_agg=mean(R1..RT)
279
- # This is parameter-free and replaces both question-anchored state
280
- # replacement and final-state-only decoding.
281
- self.raw_mean_aggregation = bool(raw_mean_aggregation)
282
- # A non-empty sequence maps the first T-1 recurrent steps to frozen
283
- # visual-token states from the listed language layers. The final step
284
- # uses Q* as memory to integrate the staged reads without another image
285
- # access. One CrossAttention module is shared by all T steps.
286
- self.hierarchical_visual_layers = [
287
- int(layer) for layer in (hierarchical_visual_layers or ())
288
- ]
289
- # Counterfactual Residual Recurrence (CRR): the image-conditioned and
290
- # text-only question states are propagated by the same native layer
291
- # F=L(ell_star+1). Only their counterfactual residual is recurrently
292
- # refined; there are no slots, V* memory, custom attention modules, or
293
- # inference-time auxiliary modules. beta is the fixed relaxed-update
294
- # coefficient; an optional batch-swap reliance loss is training-only.
295
- self.counterfactual_residual_recurrence = bool(
296
- counterfactual_residual_recurrence
297
- )
298
- # Causal Visual Residual Recurrence (CVRR), implemented as the
299
- # vision-native CRR transition. At every recurrent step the same
300
- # native layer is evaluated with and without only Q->vision attention;
301
- # their difference is the image-caused update. It deliberately
302
- # reuses CRR's strict Q-shaped decoder/cache interface and mean
303
- # aggregation, hence the parent CRR flag remains explicit.
304
- self.visual_counterfactual_recurrence = bool(
305
- visual_counterfactual_recurrence
306
- )
307
- # Minimal cumulative CVRR variant. Unlike the historical anchored
308
- # mean path, visual updates are accumulated into the running state and
309
- # the final state is decoded directly:
310
- # R_k = R_{k-1} + beta * Delta_k^V, decode R_T.
311
- # One explicit flag controls both choices so a checkpoint cannot
312
- # silently combine cumulative dynamics with the old mean interface.
313
- self.visual_cumulative_recurrence = bool(
314
- visual_cumulative_recurrence
315
- )
316
- # Full-state CVRR keeps the native recurrent computation instead of
317
- # treating the visual on/off intervention difference as the next
318
- # hidden state:
319
- # R_tilde_k = Pi_Q F([E; R_{k-1}])
320
- # R_k = R_{k-1} + beta * (R_tilde_k - R_{k-1}).
321
- # The visual-off branch remains available to evaluation code, but is
322
- # not part of this model's train/inference transition.
323
- self.visual_full_state_recurrence = bool(
324
- visual_full_state_recurrence
325
- )
326
- # Input-grounded closure control. Later recurrent calls rebuild the
327
- # native post-L20 scaffold and inject only the carried question-state
328
- # deviation, rather than feeding post-L21 rows back into L21.
329
- self.visual_boundary_reentry = bool(visual_boundary_reentry)
330
- # Source-centered boundary recurrence. The text-only L21 state is a
331
- # fixed anchor, while the native multimodal residual initializes a
332
- # nonzero, image-dependent deviation. The recurrent cell evolves
333
- # only that deviation after subtracting its zero-deviation response.
334
- self.visual_source_centered_reentry = bool(
335
- visual_source_centered_reentry
336
- )
337
- # Restrict LoRA to the single shared recurrent cell L(ell_star+1).
338
- # Its execution is disabled for R1, the text-only cache, answer-token
339
- # decoding, and L22+; only recurrent transitions k>=2 enable it.
340
- self.recurrence_only_adapter = bool(recurrence_only_adapter)
341
- # Preserve the pretrained image-caused transition while removing the
342
- # frozen layer's non-visual recurrent drift:
343
- # Delta_k = F_{base+LoRA,on}(E,R_{k-1}) - F_{base,off}(E,R_{k-1})
344
- # = (F_{base,on} - F_{base,off})
345
- # + (F_{base+LoRA,on} - F_{base,on}).
346
- # This is a full-state recurrence subtype and introduces no new
347
- # parameters or inference-time module.
348
- self.visual_preserving_adapter_correction = bool(
349
- visual_preserving_adapter_correction
350
- )
351
- # Local Visual Latent Chain (LVLC): K homogeneous latent-token
352
- # positions are evaluated in one native Transformer forward. The
353
- # first latent sees clean Q plus visual source rows; every later latent
354
- # sees only the immediately preceding latent plus the same visual
355
- # rows; answer rows see clean Q plus the final latent. This is a local
356
- # attention graph, not a recurrent forward or temporal aggregation.
357
- self.local_visual_latent_chain = bool(local_visual_latent_chain)
358
- # Perceive--Deliberate Latent Chain (PDLC). K must be even and is
359
- # interpreted as K/2 native LOOK/THINK pairs. The complete VLM stack
360
- # runs once over a block-sparse graph: LOOK rows may inspect the
361
- # multimodal prompt, THINK rows may inspect only clean question rows
362
- # and their paired LOOK, and answer rows may inspect only clean
363
- # question rows and THINK states. There is no recurrent cell, visual
364
- # state update, split-state splice, or temporal aggregator.
365
- # LVLC reuses the same one-pass latent-sequence execution plumbing but
366
- # replaces the LOOK/THINK token types and visibility graph entirely.
367
- self.perceive_deliberate_chain = bool(
368
- perceive_deliberate_chain or self.local_visual_latent_chain
369
- )
370
- # Tight PDLC graph: pair 1 bootstraps from prompt/question, later
371
- # LOOK rows receive visual placeholders plus the preceding latent
372
- # prefix, later THINK rows receive only that causal latent prefix, and
373
- # the answer consumes final THINK. It changes visibility only and
374
- # allocates no additional trainable module.
375
- self.pdlc_transition_conditioned = bool(
376
- pdlc_transition_conditioned
377
- )
378
- # Strict inference uses 1: only the bootstrap pair reads the clean
379
- # question. Larger values are an explicit evaluation intervention.
380
- # The training-only curriculum changes a runtime override and always
381
- # returns to 1 for validation/inference.
382
- self.pdlc_question_visible_through_pair = int(
383
- pdlc_question_visible_through_pair
384
- )
385
- self.pdlc_question_curriculum = bool(pdlc_question_curriculum)
386
- # Both learned latent token types start from one native text embedding
387
- # (the launcher supplies the tokenizer's newline id). -1 falls back
388
- # to token 0 for synthetic/unit-test configs only.
389
- self.latent_chain_init_token_id = int(latent_chain_init_token_id)
390
- # Spatial Recurrent Visual Field (SRVF). Unlike all historical
391
- # ell_star methods, the recurrent state is the complete native visual
392
- # token grid immediately after the vision merger. The final field is
393
- # inserted into the ordinary Qwen prefix and the full decoder runs from
394
- # scratch, making the zero-output initialization exactly base-equivalent.
395
- self.spatial_visual_recurrence = bool(spatial_visual_recurrence)
396
- self.spatial_recurrence_steps = int(spatial_recurrence_steps)
397
- self.spatial_recurrence_inner_width = int(
398
- spatial_recurrence_inner_width
399
- )
400
- self.spatial_recurrence_heads = int(spatial_recurrence_heads)
401
- # -1 uses the trained/default horizon. Non-negative values are an
402
- # evaluation-only prefix sweep, with 0 being the exact base path.
403
- self.spatial_step_override = int(spatial_step_override)
404
- self.counterfactual_beta = float(counterfactual_beta)
405
- # Learn one feature-wise mixture of the complete recurrent residual
406
- # trajectory. The bias-free T*d -> d map starts from
407
- # [I/T, ..., I/T], so enabling it is exactly the historical CRR mean
408
- # interface before the first optimizer step. A dedicated flag keeps
409
- # old CRR checkpoints and the historical slot-ER aggregator distinct.
410
- self.crr_concat_aggregation = bool(crr_concat_aggregation)
411
- # Optional latent-only policy adapter. It corrects the deterministic
412
- # C2..CT means but never touches B, C1, or answer-token decoding. Rank 0
413
- # is the exact historical CRR path; RL checkpoints serialize rank > 0.
414
- self.crr_policy_rank = int(crr_policy_rank)
415
- self.crr_policy_alpha = float(crr_policy_alpha)
416
- # Training-only CRR intervention. A batch-matched residual from a
417
- # different-answer example replaces the factual residual before the
418
- # upper decoder. The model returns the factual-minus-swapped answer
419
- # score gap; the Trainer applies the margin loss. Evaluation and
420
- # generation never execute the extra branch.
421
- self.counterfactual_reliance_loss = bool(
422
- counterfactual_reliance_loss
423
- )
424
- # Replace the aggregate residual swap with one intervention inside the
425
- # recurrent chain. C_k is swapped and C_{k+1:T} is rolled out again,
426
- # so the answer gap is attributable to a selected transition rather
427
- # than to an undifferentiated full-trajectory replacement.
428
- self.transition_aware_reliance_loss = bool(
429
- transition_aware_reliance_loss
430
- )
431
- # Training-only hard-case corrective ranking. At one selected
432
- # transition k>=2, the actual decoder scores Z_{k-1}, Z_k, and a
433
- # donor-swapped Z_k. The Trainer requires the factual transition to
434
- # improve over both detached comparators. This replaces, rather than
435
- # stacks with, the historical transition-aware reliance objective.
436
- self.corrective_transition_reliance_loss = bool(
437
- corrective_transition_reliance_loss
438
- )
439
- # Training-only functional no-regression objective. Gold-answer NLL
440
- # is measured through the actual upper decoder at every recurrent CRR
441
- # prefix; inference remains the unchanged T-step mean interface.
442
- self.counterfactual_step_retention_loss = bool(
443
- counterfactual_step_retention_loss
444
- )
445
- # Training-only self-distillation target. The multimodal lower pass
446
- # already computed for V* also provides its image-conditioned question
447
- # rows. They supervise R_agg at the split boundary, but are never
448
- # exposed to the inference decoder.
449
- self.functional_state_loss = bool(functional_state_loss)
450
- # Token CE lets long caption answers dominate one-token visual-choice
451
- # examples. The per-example variant first averages valid answer tokens
452
- # within each sample, then averages samples. It changes training/eval
453
- # loss reduction only and has no inference-time effect.
454
- self.per_example_answer_loss = bool(per_example_answer_loss)
455
- # ER v2 (advisor 2026-08-11): explicit evidence read —
456
- # R^(k) = f_theta(R^(k-1), Q*; E), E는 별도 memory로 cross-attn 읽기.
457
- # (E+R 합산-감산 제거. evidence_reasoning=True 전제.)
458
- self.explicit_evidence_read = bool(kwargs.pop("explicit_evidence_read", False))
459
- # Historical field name retained for checkpoint compatibility. In ER
460
- # v2.2 this means R^(0) is a batch-broadcast set of learnable slots; it
461
- # is deliberately NOT initialized from Q*. Q* conditions every shared
462
- # transition instead.
463
- self.r_init_question = bool(kwargs.pop("r_init_question", False))
464
- # ER v2.1 (advisor 2026-08-11): evidence read의 competitive normalization —
465
- # slot 축 softmax 후 evidence 축 재정규화 (Slot Attention식 경쟁).
466
- # 파라미터/모듈/loss 무추가; evidence_read의 W_qkv/W_o 재사용.
467
- self.competitive_evidence_read = bool(
468
- kwargs.pop("competitive_evidence_read", False))
469
- # ER v2.2 C: k>=1 traj 감독 부재 시 twin 체인 제거 (states = answer 체인).
470
- self.er_single_chain = bool(kwargs.pop("er_single_chain", False))
471
- # ER v2.2 C (§5-7): decoder latent = softmax(temporal_logits)로 가중한
472
- # R1..RT 결합 (전역 스칼라 T개). er_single_chain 전제. (legacy)
473
- self.temporal_aggregation = bool(kwargs.pop("temporal_aggregation", False))
474
- # ER v2.2 C 확정판: R_agg = Linear(Concat(R1..RT), bias=False),
475
- # 초기 W=[I/T,...,I/T] (R_agg=mean(R1..RT)). er_single_chain 전제.
476
- self.concat_aggregation = bool(kwargs.pop("concat_aggregation", False))
477
- # ER v2: 워크스페이스 블록의 rank-r 부분공간 attention/FFN (0 = dense).
478
- # d_w=d에서 state width 유지와 파라미터 폭증을 분리 (advisor).
479
- self.workspace_low_rank = int(kwargs.pop("workspace_low_rank", 0))
480
- # advisor: single-layer text-side interface alignment (bridge-only).
481
- # teacher = frozen-base mm 경로 q_end@l*+1 (adapter off, sg);
482
- # student = detach(R^T) splice의 l*+1 한 층 재계산. 추론 시 무존재.
483
- self.interface_loss = interface_loss
484
- # Training-only visual anchor switch. False in the current answer-only
485
- # method; when false the model does not even materialize pooled V* for
486
- # the Trainer.
487
- self.read_anchor = bool(read_anchor)
488
- if evidence_reasoning and persistent_slot_id:
489
- raise ValueError("evidence_reasoning은 persistent_slot_id와 동시 사용 불가")
490
- if evidence_reasoning and not mid_read_step:
491
- raise ValueError("evidence_reasoning은 mid_read_step > 0 필요")
492
- self.validate()
493
-
494
- # -- derived views -----------------------------------------------------
495
-
496
- @property
497
- def num_decoder_layers(self) -> int:
498
- return self.text_config.num_hidden_layers
499
-
500
- @property
501
- def backbone_width(self) -> int:
502
- """``d`` -- backbone hidden width (3584 for the 7B)."""
503
- return self.text_config.hidden_size
504
-
505
- @property
506
- def lower_slice(self) -> slice:
507
- """Layers forming ``F_{<=l*}``."""
508
- self._require_ell_star()
509
- return slice(0, self.ell_star + 1)
510
-
511
- @property
512
- def upper_slice(self) -> slice:
513
- """Layers forming ``F_{>l*}``."""
514
- self._require_ell_star()
515
- return slice(self.ell_star + 1, self.num_decoder_layers)
516
-
517
- # -- validation --------------------------------------------------------
518
-
519
- def _require_ell_star(self) -> None:
520
- if self.ell_star is None:
521
- raise ValueError(
522
- "ell_star is unset. Run scripts/localize_read_point.py (Method 3.1) "
523
- "and pass the measured layer explicitly; it must not be guessed."
524
- )
525
-
526
- def validate(self) -> None:
527
- """Reject configurations that silently break the §3.2 contract."""
528
- # ``PretrainedConfig.from_pretrained`` may apply user overrides with
529
- # setattr *after* __init__. In that path, setting LVLC=True would not
530
- # re-run the constructor's promotion of the shared one-pass execution
531
- # flag. Re-establish the invariant before any model reads the config.
532
- if getattr(self, "local_visual_latent_chain", False):
533
- self.perceive_deliberate_chain = True
534
- if self.ell_star is not None:
535
- n = self.num_decoder_layers
536
- # Both branches must be non-empty: an empty lower branch means there
537
- # is no V* to read, an empty upper branch means the workspace never
538
- # reaches the decoder.
539
- if not 0 <= self.ell_star <= n - 2:
540
- raise ValueError(
541
- f"ell_star={self.ell_star} out of range for {n} decoder layers; "
542
- f"expected 0 <= ell_star <= {n - 2} so both branches are non-empty."
543
- )
544
- # K=0 is the no-recurrence control (read only), not a misconfiguration.
545
- if self.num_workspace_steps < 0:
546
- raise ValueError("num_workspace_steps (K) must be >= 0.")
547
- if self.num_workspace_slots < 1:
548
- raise ValueError("num_workspace_slots (S) must be >= 1.")
549
- if self.workspace_num_heads < 1:
550
- raise ValueError("workspace_num_heads must be >= 1.")
551
- if self.workspace_width % self.workspace_num_heads != 0:
552
- raise ValueError(
553
- f"workspace_width={self.workspace_width} must be divisible by "
554
- f"workspace_num_heads={self.workspace_num_heads}."
555
- )
556
- if self.workspace_width > self.backbone_width:
557
- raise ValueError(
558
- f"workspace_width (d_w={self.workspace_width}) cannot exceed "
559
- f"backbone width (d={self.backbone_width}). Native-width d_w=d is "
560
- "the current method and uses an Identity decoder interface."
561
- )
562
- if self.workspace_rope_mode not in WORKSPACE_ROPE_MODES:
563
- raise ValueError(
564
- f"workspace_rope_mode={self.workspace_rope_mode!r} not in "
565
- f"{WORKSPACE_ROPE_MODES}."
566
- )
567
- if self.adapter_rank < 0:
568
- raise ValueError("adapter_rank must be >= 0 (0 disables the adapter).")
569
- if not math.isfinite(self.adapter_dropout) or not 0.0 <= self.adapter_dropout < 1.0:
570
- raise ValueError("adapter_dropout must be finite and in [0, 1).")
571
- if getattr(self, "crr_policy_rank", 0) < 0:
572
- raise ValueError("crr_policy_rank must be >= 0.")
573
- if not math.isfinite(getattr(self, "crr_policy_alpha", 0.0)) or getattr(
574
- self, "crr_policy_alpha", 0.0
575
- ) <= 0.0:
576
- raise ValueError("crr_policy_alpha must be finite and > 0.")
577
- if self.read_num_blocks < 1 or self.transition_num_blocks < 1:
578
- raise ValueError("read_num_blocks and transition_num_blocks must be >= 1.")
579
- if self.workspace_ffn_mult < 1:
580
- raise ValueError("workspace_ffn_mult must be >= 1.")
581
- mid = int(getattr(self, "mid_read_step", 0) or 0)
582
- if mid < 0 or mid > self.num_workspace_steps:
583
- raise ValueError(
584
- f"mid_read_step={mid} must be in [0, num_workspace_steps="
585
- f"{self.num_workspace_steps}]."
586
- )
587
- low_rank = int(getattr(self, "workspace_low_rank", 0) or 0)
588
- if low_rank < 0:
589
- raise ValueError("workspace_low_rank must be >= 0.")
590
- if low_rank and low_rank % self.workspace_num_heads != 0:
591
- raise ValueError(
592
- f"workspace_low_rank={low_rank} must be divisible by "
593
- f"workspace_num_heads={self.workspace_num_heads}."
594
- )
595
- if getattr(self, "competitive_evidence_read", False) and not getattr(
596
- self, "explicit_evidence_read", False
597
- ):
598
- raise ValueError(
599
- "competitive_evidence_read requires explicit_evidence_read=True; "
600
- "otherwise the flag has no execution path."
601
- )
602
- if getattr(self, "explicit_evidence_read", False) and not getattr(
603
- self, "evidence_reasoning", False
604
- ):
605
- raise ValueError(
606
- "explicit_evidence_read requires evidence_reasoning=True; otherwise "
607
- "the evidence-read module is allocated but never called."
608
- )
609
- if getattr(self, "r_init_question", False) and not getattr(
610
- self, "evidence_reasoning", False
611
- ):
612
- raise ValueError(
613
- "r_init_question (legacy name for learnable R0 slots) requires "
614
- "evidence_reasoning=True; otherwise reasoning_slots are unused."
615
- )
616
- if (getattr(self, "concat_aggregation", False)
617
- or getattr(self, "temporal_aggregation", False)):
618
- if self.num_workspace_steps < 1:
619
- raise ValueError("reasoning aggregation requires num_workspace_steps >= 1.")
620
- if not getattr(self, "evidence_reasoning", False):
621
- raise ValueError(
622
- "reasoning aggregation requires evidence_reasoning=True; "
623
- "otherwise the aggregation module is unused."
624
- )
625
- if getattr(self, "concat_aggregation", False) and not getattr(
626
- self, "er_single_chain", False
627
- ):
628
- raise ValueError("concat_aggregation requires er_single_chain=True.")
629
- if getattr(self, "raw_evidence_question", False):
630
- if self.workspace_width != self.backbone_width:
631
- raise ValueError(
632
- "raw_evidence_question requires native workspace_width == "
633
- "backbone_width: E=V* and R0=Q* are used without projections."
634
- )
635
- if self.num_workspace_steps < 1:
636
- raise ValueError(
637
- "raw_evidence_question requires num_workspace_steps >= 1."
638
- )
639
- source_layer = (
640
- self.ell_star + 1
641
- if self.raw_attention_source_layer < 0
642
- else self.raw_attention_source_layer
643
- )
644
- if not self.ell_star < source_layer < self.text_config.num_hidden_layers:
645
- raise ValueError(
646
- "raw_attention_source_layer must be an upper-backbone layer, "
647
- f"got ell_star={self.ell_star}, source={source_layer}."
648
- )
649
- incompatible = {
650
- "evidence_reasoning": bool(getattr(self, "evidence_reasoning", False)),
651
- "explicit_evidence_read": bool(getattr(self, "explicit_evidence_read", False)),
652
- "competitive_evidence_read": bool(getattr(self, "competitive_evidence_read", False)),
653
- "r_init_question": bool(getattr(self, "r_init_question", False)),
654
- "er_single_chain": bool(getattr(self, "er_single_chain", False)),
655
- "temporal_aggregation": bool(getattr(self, "temporal_aggregation", False)),
656
- "concat_aggregation": bool(getattr(self, "concat_aggregation", False)),
657
- "read_shortcut": bool(getattr(self, "read_shortcut", False)),
658
- "splice_scale_match": bool(getattr(self, "splice_scale_match", False)),
659
- "read_gating": bool(getattr(self, "read_gating", False)),
660
- "read_residual_pure": bool(getattr(self, "read_residual_pure", False)),
661
- "final_verify": bool(getattr(self, "final_verify", False)),
662
- "mid_read_step": bool(getattr(self, "mid_read_step", 0)),
663
- "persistent_slot_id": bool(getattr(self, "persistent_slot_id", False)),
664
- "interface_loss": bool(getattr(self, "interface_loss", False)),
665
- "read_anchor": bool(getattr(self, "read_anchor", False)),
666
- }
667
- active = [name for name, enabled in incompatible.items() if enabled]
668
- if active:
669
- raise ValueError(
670
- "raw_evidence_question replaces the slot workspace and is "
671
- f"incompatible with: {', '.join(active)}"
672
- )
673
- if self.raw_state_replacement and self.raw_transition_ffn:
674
- raise ValueError(
675
- "raw_state_replacement is CrossAttn-only and is incompatible "
676
- "with raw_transition_ffn=True."
677
- )
678
- if self.raw_question_anchor and not self.raw_state_replacement:
679
- raise ValueError(
680
- "raw_question_anchor requires raw_state_replacement=True."
681
- )
682
- if self.raw_mean_aggregation and (
683
- self.raw_state_replacement or self.raw_question_anchor
684
- ):
685
- raise ValueError(
686
- "raw_mean_aggregation replaces raw_state_replacement and "
687
- "raw_question_anchor; use the residual recurrence."
688
- )
689
- hierarchy = list(
690
- getattr(self, "hierarchical_visual_layers", ()) or ()
691
- )
692
- if hierarchy:
693
- if not self.raw_mean_aggregation:
694
- raise ValueError(
695
- "hierarchical_visual_layers requires "
696
- "raw_mean_aggregation=True."
697
- )
698
- if self.raw_transition_ffn:
699
- raise ValueError(
700
- "hierarchical visual recurrence is CrossAttn-only and "
701
- "requires raw_transition_ffn=False."
702
- )
703
- expected_visual_reads = self.num_workspace_steps - 1
704
- if len(hierarchy) != expected_visual_reads:
705
- raise ValueError(
706
- "hierarchical_visual_layers must contain exactly one "
707
- "layer per visual-read step, followed by one Q* "
708
- "integration step: "
709
- f"got {len(hierarchy)} layers for "
710
- f"T={self.num_workspace_steps} "
711
- f"(expected {expected_visual_reads})."
712
- )
713
- if hierarchy != sorted(set(hierarchy)):
714
- raise ValueError(
715
- "hierarchical_visual_layers must be strictly increasing."
716
- )
717
- invalid = [
718
- layer
719
- for layer in hierarchy
720
- if not 0 <= layer <= int(self.ell_star)
721
- ]
722
- if invalid:
723
- raise ValueError(
724
- "hierarchical_visual_layers must lie in the frozen lower "
725
- f"branch [0,{self.ell_star}], got {invalid}."
726
- )
727
- if self.functional_state_loss:
728
- raise ValueError(
729
- "functional_state_loss is not defined for hierarchical "
730
- "visual recurrence; use final-answer CE only."
731
- )
732
- if self.functional_state_loss and not self.raw_mean_aggregation:
733
- raise ValueError(
734
- "functional_state_loss requires raw_mean_aggregation=True "
735
- "so its target is the exact decoder-facing recurrence."
736
- )
737
- elif getattr(self, "raw_state_replacement", False):
738
- raise ValueError(
739
- "raw_state_replacement requires raw_evidence_question=True."
740
- )
741
- elif getattr(self, "raw_question_anchor", False):
742
- raise ValueError(
743
- "raw_question_anchor requires raw_evidence_question=True."
744
- )
745
- elif getattr(self, "raw_mean_aggregation", False):
746
- raise ValueError(
747
- "raw_mean_aggregation requires raw_evidence_question=True."
748
- )
749
- elif getattr(self, "hierarchical_visual_layers", ()):
750
- raise ValueError(
751
- "hierarchical_visual_layers requires "
752
- "raw_evidence_question=True."
753
- )
754
- elif getattr(self, "functional_state_loss", False):
755
- raise ValueError(
756
- "functional_state_loss requires raw_evidence_question=True."
757
- )
758
- if (
759
- getattr(self, "counterfactual_reliance_loss", False)
760
- and not getattr(self, "counterfactual_residual_recurrence", False)
761
- ):
762
- raise ValueError(
763
- "counterfactual_reliance_loss requires "
764
- "counterfactual_residual_recurrence=True."
765
- )
766
- if (
767
- getattr(self, "transition_aware_reliance_loss", False)
768
- and not getattr(self, "counterfactual_reliance_loss", False)
769
- ):
770
- raise ValueError(
771
- "transition_aware_reliance_loss requires "
772
- "counterfactual_reliance_loss=True."
773
- )
774
- if (
775
- getattr(self, "transition_aware_reliance_loss", False)
776
- and self.num_workspace_steps < 2
777
- ):
778
- raise ValueError(
779
- "transition_aware_reliance_loss requires at least two steps."
780
- )
781
- if (
782
- getattr(self, "corrective_transition_reliance_loss", False)
783
- and not getattr(self, "counterfactual_reliance_loss", False)
784
- ):
785
- raise ValueError(
786
- "corrective_transition_reliance_loss requires "
787
- "counterfactual_reliance_loss=True."
788
- )
789
- if (
790
- getattr(self, "corrective_transition_reliance_loss", False)
791
- and self.num_workspace_steps < 2
792
- ):
793
- raise ValueError(
794
- "corrective_transition_reliance_loss requires at least two steps."
795
- )
796
- if (
797
- getattr(self, "corrective_transition_reliance_loss", False)
798
- and getattr(self, "transition_aware_reliance_loss", False)
799
- ):
800
- raise ValueError(
801
- "corrective_transition_reliance_loss replaces, and cannot be "
802
- "combined with, transition_aware_reliance_loss."
803
- )
804
- if (
805
- getattr(self, "corrective_transition_reliance_loss", False)
806
- and getattr(self, "counterfactual_step_retention_loss", False)
807
- ):
808
- raise ValueError(
809
- "corrective_transition_reliance_loss replaces, and cannot be "
810
- "combined with, counterfactual_step_retention_loss."
811
- )
812
- if (
813
- getattr(self, "counterfactual_step_retention_loss", False)
814
- and not getattr(self, "counterfactual_residual_recurrence", False)
815
- ):
816
- raise ValueError(
817
- "counterfactual_step_retention_loss requires "
818
- "counterfactual_residual_recurrence=True."
819
- )
820
- if (
821
- getattr(self, "counterfactual_step_retention_loss", False)
822
- and self.num_workspace_steps < 2
823
- ):
824
- raise ValueError(
825
- "counterfactual_step_retention_loss requires at least two steps."
826
- )
827
- if getattr(self, "crr_concat_aggregation", False) and not getattr(
828
- self, "counterfactual_residual_recurrence", False
829
- ):
830
- raise ValueError(
831
- "crr_concat_aggregation requires "
832
- "counterfactual_residual_recurrence=True."
833
- )
834
- if getattr(self, "counterfactual_residual_recurrence", False):
835
- if getattr(self, "raw_evidence_question", False):
836
- raise ValueError(
837
- "counterfactual_residual_recurrence replaces "
838
- "raw_evidence_question; enable exactly one q-state method."
839
- )
840
- if self.num_workspace_steps < 1:
841
- raise ValueError(
842
- "counterfactual_residual_recurrence requires "
843
- "num_workspace_steps >= 1."
844
- )
845
- if self.crr_policy_rank > 0 and self.num_workspace_steps < 2:
846
- raise ValueError(
847
- "crr_policy_rank > 0 requires num_workspace_steps >= 2."
848
- )
849
- if not 0.0 <= self.counterfactual_beta <= 1.0:
850
- raise ValueError(
851
- "counterfactual_beta must lie in [0, 1], got "
852
- f"{self.counterfactual_beta}."
853
- )
854
- incompatible = {
855
- "evidence_reasoning": bool(getattr(self, "evidence_reasoning", False)),
856
- "explicit_evidence_read": bool(getattr(self, "explicit_evidence_read", False)),
857
- "competitive_evidence_read": bool(getattr(self, "competitive_evidence_read", False)),
858
- "r_init_question": bool(getattr(self, "r_init_question", False)),
859
- "er_single_chain": bool(getattr(self, "er_single_chain", False)),
860
- "temporal_aggregation": bool(getattr(self, "temporal_aggregation", False)),
861
- "concat_aggregation": bool(getattr(self, "concat_aggregation", False)),
862
- "read_shortcut": bool(getattr(self, "read_shortcut", False)),
863
- "splice_scale_match": bool(getattr(self, "splice_scale_match", False)),
864
- "read_gating": bool(getattr(self, "read_gating", False)),
865
- "read_residual_pure": bool(getattr(self, "read_residual_pure", False)),
866
- "final_verify": bool(getattr(self, "final_verify", False)),
867
- "mid_read_step": bool(getattr(self, "mid_read_step", 0)),
868
- "persistent_slot_id": bool(getattr(self, "persistent_slot_id", False)),
869
- "interface_loss": bool(getattr(self, "interface_loss", False)),
870
- "read_anchor": bool(getattr(self, "read_anchor", False)),
871
- "functional_state_loss": bool(getattr(self, "functional_state_loss", False)),
872
- }
873
- active = [name for name, enabled in incompatible.items() if enabled]
874
- if active:
875
- raise ValueError(
876
- "counterfactual_residual_recurrence replaces the slot/raw "
877
- f"workspace and is incompatible with: {', '.join(active)}"
878
- )
879
- elif getattr(self, "crr_policy_rank", 0) > 0:
880
- raise ValueError(
881
- "crr_policy_rank > 0 requires "
882
- "counterfactual_residual_recurrence=True."
883
- )
884
- if getattr(self, "visual_counterfactual_recurrence", False):
885
- if not getattr(
886
- self, "counterfactual_residual_recurrence", False
887
- ):
888
- raise ValueError(
889
- "visual_counterfactual_recurrence requires "
890
- "counterfactual_residual_recurrence=True."
891
- )
892
- # CVRR is the intentionally minimal CE-only candidate. The old
893
- # CRR tail-rollout losses/policy implement a different transition
894
- # equation and must not be silently applied to this trajectory.
895
- incompatible = {
896
- "counterfactual_reliance_loss": bool(
897
- getattr(self, "counterfactual_reliance_loss", False)
898
- ),
899
- "transition_aware_reliance_loss": bool(
900
- getattr(self, "transition_aware_reliance_loss", False)
901
- ),
902
- "corrective_transition_reliance_loss": bool(
903
- getattr(self, "corrective_transition_reliance_loss", False)
904
- ),
905
- "counterfactual_step_retention_loss": bool(
906
- getattr(self, "counterfactual_step_retention_loss", False)
907
- ),
908
- "crr_concat_aggregation": bool(
909
- getattr(self, "crr_concat_aggregation", False)
910
- ),
911
- "crr_policy_rank": bool(getattr(self, "crr_policy_rank", 0)),
912
- }
913
- active = [name for name, enabled in incompatible.items() if enabled]
914
- if active:
915
- raise ValueError(
916
- "visual_counterfactual_recurrence uses final-answer CE only "
917
- "and a parameter-free decoder interface; incompatible with: "
918
- + ", ".join(active)
919
- )
920
- elif getattr(self, "visual_cumulative_recurrence", False):
921
- raise ValueError(
922
- "visual_cumulative_recurrence requires "
923
- "visual_counterfactual_recurrence=True."
924
- )
925
- if getattr(self, "visual_full_state_recurrence", False):
926
- if not getattr(self, "visual_counterfactual_recurrence", False):
927
- raise ValueError(
928
- "visual_full_state_recurrence requires "
929
- "visual_counterfactual_recurrence=True."
930
- )
931
- if not getattr(self, "visual_cumulative_recurrence", False):
932
- raise ValueError(
933
- "visual_full_state_recurrence requires "
934
- "visual_cumulative_recurrence=True."
935
- )
936
- if getattr(self, "visual_boundary_reentry", False) and not getattr(
937
- self, "visual_full_state_recurrence", False
938
- ):
939
- raise ValueError(
940
- "visual_boundary_reentry requires "
941
- "visual_full_state_recurrence=True."
942
- )
943
- if getattr(self, "visual_source_centered_reentry", False):
944
- if not getattr(self, "visual_boundary_reentry", False):
945
- raise ValueError(
946
- "visual_source_centered_reentry requires "
947
- "visual_boundary_reentry=True."
948
- )
949
- if getattr(self, "visual_preserving_adapter_correction", False):
950
- raise ValueError(
951
- "visual_source_centered_reentry cannot be combined with "
952
- "visual_preserving_adapter_correction."
953
- )
954
- if getattr(self, "recurrence_only_adapter", False):
955
- if not getattr(self, "visual_full_state_recurrence", False):
956
- raise ValueError(
957
- "recurrence_only_adapter requires "
958
- "visual_full_state_recurrence=True."
959
- )
960
- if self.adapter_rank <= 0:
961
- raise ValueError(
962
- "recurrence_only_adapter requires adapter_rank > 0."
963
- )
964
- if self.ell_star is None:
965
- raise ValueError(
966
- "recurrence_only_adapter requires an explicit ell_star."
967
- )
968
- recurrent_layer = int(self.ell_star) + 1
969
- if recurrent_layer in set(self.adapter_exclude_layers):
970
- raise ValueError(
971
- "recurrence_only_adapter cannot exclude its recurrent "
972
- f"layer {recurrent_layer} from LoRA."
973
- )
974
- if getattr(self, "visual_preserving_adapter_correction", False):
975
- if not getattr(self, "visual_full_state_recurrence", False):
976
- raise ValueError(
977
- "visual_preserving_adapter_correction requires "
978
- "visual_full_state_recurrence=True."
979
- )
980
- if not getattr(self, "recurrence_only_adapter", False):
981
- raise ValueError(
982
- "visual_preserving_adapter_correction requires "
983
- "recurrence_only_adapter=True."
984
- )
985
- if getattr(self, "perceive_deliberate_chain", False):
986
- if self.num_workspace_steps < 2 or self.num_workspace_steps % 2:
987
- raise ValueError(
988
- "one-pass latent chains require an even "
989
- "num_workspace_steps >= 2."
990
- )
991
- vocab_size = int(self.text_config.vocab_size)
992
- if not -1 <= self.latent_chain_init_token_id < vocab_size:
993
- raise ValueError(
994
- "latent_chain_init_token_id must be -1 or a valid text "
995
- f"token id below {vocab_size}."
996
- )
997
- num_pairs = self.num_workspace_steps // 2
998
- if self.local_visual_latent_chain and (
999
- self.pdlc_transition_conditioned
1000
- or self.pdlc_question_curriculum
1001
- or self.pdlc_question_visible_through_pair != 1
1002
- ):
1003
- raise ValueError(
1004
- "local_visual_latent_chain replaces PDLC transition and "
1005
- "question-visibility controls"
1006
- )
1007
- if not 1 <= self.pdlc_question_visible_through_pair <= num_pairs:
1008
- raise ValueError(
1009
- "pdlc_question_visible_through_pair must lie in "
1010
- f"[1,{num_pairs}]"
1011
- )
1012
- if self.pdlc_question_curriculum and not (
1013
- getattr(self, "pdlc_transition_conditioned", False)
1014
- ):
1015
- raise ValueError(
1016
- "pdlc_question_curriculum requires "
1017
- "pdlc_transition_conditioned=True"
1018
- )
1019
- incompatible = {
1020
- "raw_evidence_question": bool(
1021
- getattr(self, "raw_evidence_question", False)
1022
- ),
1023
- "counterfactual_residual_recurrence": bool(
1024
- getattr(self, "counterfactual_residual_recurrence", False)
1025
- ),
1026
- "spatial_visual_recurrence": bool(
1027
- getattr(self, "spatial_visual_recurrence", False)
1028
- ),
1029
- "evidence_reasoning": bool(
1030
- getattr(self, "evidence_reasoning", False)
1031
- ),
1032
- "read_gating": bool(getattr(self, "read_gating", False)),
1033
- "read_residual_pure": bool(
1034
- getattr(self, "read_residual_pure", False)
1035
- ),
1036
- "final_verify": bool(getattr(self, "final_verify", False)),
1037
- "mid_read_step": bool(getattr(self, "mid_read_step", 0)),
1038
- "interface_loss": bool(getattr(self, "interface_loss", False)),
1039
- "read_anchor": bool(getattr(self, "read_anchor", False)),
1040
- "counterfactual_reliance_loss": bool(
1041
- getattr(self, "counterfactual_reliance_loss", False)
1042
- ),
1043
- "counterfactual_step_retention_loss": bool(
1044
- getattr(self, "counterfactual_step_retention_loss", False)
1045
- ),
1046
- "functional_state_loss": bool(
1047
- getattr(self, "functional_state_loss", False)
1048
- ),
1049
- }
1050
- active = [name for name, enabled in incompatible.items() if enabled]
1051
- if active:
1052
- raise ValueError(
1053
- "perceive_deliberate_chain replaces every recurrent/"
1054
- "workspace path; incompatible with: " + ", ".join(active)
1055
- )
1056
- elif getattr(self, "pdlc_transition_conditioned", False):
1057
- raise ValueError(
1058
- "pdlc_transition_conditioned requires "
1059
- "perceive_deliberate_chain=True."
1060
- )
1061
- elif getattr(self, "pdlc_question_curriculum", False) or (
1062
- getattr(self, "pdlc_question_visible_through_pair", 1) != 1
1063
- ):
1064
- raise ValueError(
1065
- "PDLC question visibility controls require "
1066
- "perceive_deliberate_chain=True."
1067
- )
1068
- if getattr(self, "spatial_visual_recurrence", False):
1069
- if not 0.0 <= self.counterfactual_beta <= 1.0:
1070
- raise ValueError(
1071
- "counterfactual_beta must lie in [0, 1], got "
1072
- f"{self.counterfactual_beta}."
1073
- )
1074
- if self.spatial_recurrence_steps < 1:
1075
- raise ValueError(
1076
- "spatial_recurrence_steps must be >= 1 for training"
1077
- )
1078
- if self.spatial_recurrence_inner_width < 1:
1079
- raise ValueError(
1080
- "spatial_recurrence_inner_width must be positive"
1081
- )
1082
- if self.spatial_recurrence_heads < 1 or (
1083
- self.spatial_recurrence_inner_width
1084
- % self.spatial_recurrence_heads
1085
- ):
1086
- raise ValueError(
1087
- "spatial_recurrence_inner_width must be divisible by "
1088
- "spatial_recurrence_heads"
1089
- )
1090
- if self.spatial_step_override < -1:
1091
- raise ValueError("spatial_step_override must be >= -1")
1092
- if self.adapter_rank != 0:
1093
- raise ValueError(
1094
- "spatial_visual_recurrence replaces decoder LoRA; set "
1095
- "adapter_rank=0 so the T=0 path remains the frozen base model"
1096
- )
1097
- incompatible = {
1098
- "raw_evidence_question": bool(
1099
- getattr(self, "raw_evidence_question", False)
1100
- ),
1101
- "counterfactual_residual_recurrence": bool(
1102
- getattr(self, "counterfactual_residual_recurrence", False)
1103
- ),
1104
- "evidence_reasoning": bool(
1105
- getattr(self, "evidence_reasoning", False)
1106
- ),
1107
- "read_gating": bool(getattr(self, "read_gating", False)),
1108
- "read_residual_pure": bool(
1109
- getattr(self, "read_residual_pure", False)
1110
- ),
1111
- "final_verify": bool(getattr(self, "final_verify", False)),
1112
- "mid_read_step": bool(getattr(self, "mid_read_step", 0)),
1113
- "interface_loss": bool(getattr(self, "interface_loss", False)),
1114
- "read_anchor": bool(getattr(self, "read_anchor", False)),
1115
- "counterfactual_reliance_loss": bool(
1116
- getattr(self, "counterfactual_reliance_loss", False)
1117
- ),
1118
- "counterfactual_step_retention_loss": bool(
1119
- getattr(self, "counterfactual_step_retention_loss", False)
1120
- ),
1121
- "functional_state_loss": bool(
1122
- getattr(self, "functional_state_loss", False)
1123
- ),
1124
- }
1125
- active = [name for name, enabled in incompatible.items() if enabled]
1126
- if active:
1127
- raise ValueError(
1128
- "spatial_visual_recurrence replaces every historical "
1129
- "workspace/reliance path; incompatible with: "
1130
- + ", ".join(active)
1131
- )
1132
- modes = [bool(getattr(self, m, False)) for m in
1133
- ("read_gating", "read_residual_pure", "final_verify")]
1134
- modes.append(bool(getattr(self, "mid_read_step", 0)))
1135
- if sum(modes) > 1:
1136
- raise ValueError(
1137
- "read_gating / read_residual_pure / final_verify are mutually "
1138
- "exclusive reread modes."
1139
- )
1140
-
1141
-
1142
- __all__ = ["CloseQwen2_5_VLConfig", "WORKSPACE_ROPE_MODES"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configuration_source_qwen3.py DELETED
@@ -1,740 +0,0 @@
1
- """Configuration for CLOSE latent reasoning on Qwen3-VL.
2
-
3
- The current ``raw_evidence_question`` path keeps the complete layer-``ell_star``
4
- visual memory as immutable evidence and recurrently updates the text-only
5
- question state by cross-attending to it. Its selected interface averages all
6
- four residual recurrent states and may use a training-only restoration target
7
- from the normal multimodal question state. Historical slot-workspace fields are
8
- retained so old checkpoints remain loadable, but they are mutually exclusive
9
- with the raw-evidence path.
10
-
11
- Every architectural hyperparameter of Method §3 lives here -- nothing is
12
- hardcoded in ``modeling_close_qwen3_vl.py``.
13
-
14
- Layer-index convention (used consistently across the repo)
15
- ----------------------------------------------------------
16
- ``ell_star`` is **0-indexed and inclusive**: it is the index of the last decoder
17
- layer belonging to the lower branch. With ``num_hidden_layers == 28``::
18
-
19
- F_{<=l*} = language_model.layers[: ell_star + 1] # lower_slice
20
- F_{>l*} = language_model.layers[ell_star + 1 :] # upper_slice
21
-
22
- so ``ell_star=18`` means layers 0..18 run before the split and 19..27 after it.
23
- ``ell_star`` is measured once by ``scripts/localize_read_point.py`` (§3.1) and
24
- frozen before any workspace training.
25
-
26
- Careful with ``Qwen3VLConfig``: its ``__setattr__`` forwards any attribute
27
- whose name already exists in ``text_config.__dict__`` down to the sub-config, and
28
- its ``__init__`` builds ``text_config`` from ``**kwargs``. Workspace fields are
29
- therefore declared as explicit named parameters so they never enter ``kwargs``
30
- and never shadow a text-config field. ``tests/test_config_roundtrip.py`` guards
31
- this.
32
- """
33
-
34
- from __future__ import annotations
35
-
36
- import math
37
-
38
- from transformers.models.qwen3_vl.configuration_qwen3_vl import Qwen3VLConfig
39
-
40
- #: How the ``S`` workspace slots are assigned M-RoPE positions once they are
41
- #: spliced into the replacement cache above ``ell_star``. Qwen3-VL uses 3D
42
- #: M-RoPE (t, h, w) with ``mrope_section=[16, 24, 24]``, so slots -- which have
43
- #: no spatial extent -- need an explicit convention.
44
- WORKSPACE_ROPE_MODES = (
45
- # Slots continue the 1-D text position sequence directly after Q*, with
46
- # t == h == w. Treats the workspace as "more text".
47
- "continue",
48
- # Slots reuse the (t, h, w) span the image tokens occupied in the original
49
- # multimodal prefill, subsampled to S positions. Preserves the read point's
50
- # positional geometry but reintroduces image-derived indices.
51
- "image_span",
52
- # All slots share one position (the first position after Q*). Makes the
53
- # workspace order-free, matching the set semantics of Perceiver-style slots.
54
- "shared",
55
- )
56
-
57
-
58
- class CloseQwen3VLConfig(Qwen3VLConfig):
59
- r"""Configuration for :class:`CloseQwen3VLForConditionalGeneration`.
60
-
61
- Args:
62
- ell_star: 0-indexed inclusive last layer of the lower branch. ``None``
63
- until §3.1 localization has run; the model refuses to build with
64
- ``None`` rather than falling back to a guessed depth.
65
- num_workspace_steps: the fixed recurrent horizon (paper notation ``T``;
66
- per-item segment counts ``n`` exist only in the losses, never here).
67
- The trainable parameter count is independent of it because
68
- ``f_theta`` is shared across steps, so it is a free experimental
69
- axis. ``0`` is legal and is the no-recurrence floor: ``Z^(0)`` goes
70
- straight to the decoder.
71
- num_workspace_slots: ``S``, slots in ``Z``. Free capacity knob.
72
- workspace_width: ``d_w``, the slot width. Free capacity knob -- the
73
- method's claim is path exclusivity (all visual information routed
74
- through ``Z``), NOT an information bottleneck, so ``S`` and ``d_w``
75
- carry no methodological commitment. Kept below ``d`` (4096 for the
76
- 7B) since ``P_Z`` projects up at the splice.
77
- workspace_num_heads: attention heads inside ``r_theta`` / ``f_theta``.
78
- read_num_blocks: cross-attention blocks in ``r_theta``. The *read* is
79
- once regardless of depth: all blocks see the same ``V*`` in a single
80
- forward, and ``V*`` is discarded afterwards.
81
- transition_num_blocks: blocks in the shared recurrent ``f_theta``.
82
- workspace_ffn_mult: FFN expansion inside workspace blocks.
83
- workspace_dropout: dropout inside workspace blocks.
84
- workspace_rope_mode: one of :data:`WORKSPACE_ROPE_MODES`.
85
- adapter_rank: rank of the low-rank adapter on the post-``ell_star``
86
- backbone layers. ``0`` disables it (frozen backbone only).
87
- adapter_alpha: LoRA-style scaling; effective scale is
88
- ``adapter_alpha / adapter_rank``.
89
- adapter_dropout: dropout probability applied only to the LoRA branch.
90
- lambda_traj: legacy trajectory-loss weight. The current method sets it
91
- to zero and trains with final-answer CE only; it remains serialized
92
- solely so historical checkpoints can still be inspected.
93
- """
94
-
95
- model_type = "close_qwen3_vl"
96
-
97
- def __init__(
98
- self,
99
- ell_star: int | None = None,
100
- num_workspace_steps: int = 4,
101
- num_workspace_slots: int = 32,
102
- workspace_width: int = 512,
103
- workspace_num_heads: int = 8,
104
- read_num_blocks: int = 1,
105
- transition_num_blocks: int = 2,
106
- workspace_ffn_mult: int = 4,
107
- workspace_dropout: float = 0.0,
108
- workspace_rope_mode: str = "continue",
109
- adapter_rank: int = 16,
110
- adapter_alpha: int = 32,
111
- adapter_dropout: float = 0.0,
112
- adapter_exclude_layers: tuple = (),
113
- lambda_traj: float = 0.0,
114
- read_shortcut: bool = False,
115
- splice_scale_match: bool = False,
116
- read_gating: bool = False,
117
- gate_init_eps: float = 0.02,
118
- read_residual_pure: bool = False,
119
- residual_norm_cap: float = 0.0,
120
- pure_warm_start: bool = False,
121
- final_verify: bool = False,
122
- mid_read_step: int = 0,
123
- persistent_slot_id: bool = False,
124
- mid_read_alpha: float = 1.0,
125
- evidence_reasoning: bool = False,
126
- raw_evidence_question: bool = False,
127
- raw_attention_source_layer: int = -1,
128
- raw_transition_ffn: bool | None = None,
129
- raw_state_replacement: bool = False,
130
- raw_question_anchor: bool = False,
131
- raw_mean_aggregation: bool = False,
132
- counterfactual_residual_recurrence: bool = False,
133
- visual_counterfactual_recurrence: bool = False,
134
- visual_cumulative_recurrence: bool = False,
135
- visual_full_state_recurrence: bool = False,
136
- recurrence_only_adapter: bool = False,
137
- counterfactual_beta: float = 1.0,
138
- crr_concat_aggregation: bool = False,
139
- crr_policy_rank: int = 0,
140
- crr_policy_alpha: float = 32.0,
141
- counterfactual_reliance_loss: bool = False,
142
- transition_aware_reliance_loss: bool = False,
143
- counterfactual_step_retention_loss: bool = False,
144
- functional_state_loss: bool = False,
145
- per_example_answer_loss: bool = False,
146
- interface_loss: bool = False,
147
- read_anchor: bool = False,
148
- **kwargs,
149
- ):
150
- super().__init__(**kwargs)
151
- self.ell_star = ell_star
152
- self.num_workspace_steps = num_workspace_steps
153
- self.num_workspace_slots = num_workspace_slots
154
- self.workspace_width = workspace_width
155
- self.workspace_num_heads = workspace_num_heads
156
- self.read_num_blocks = read_num_blocks
157
- self.transition_num_blocks = transition_num_blocks
158
- self.workspace_ffn_mult = workspace_ffn_mult
159
- self.workspace_dropout = workspace_dropout
160
- self.workspace_rope_mode = workspace_rope_mode
161
- self.adapter_rank = adapter_rank
162
- self.adapter_alpha = adapter_alpha
163
- self.adapter_dropout = adapter_dropout
164
- # E run: dense full-FT 층은 LoRA 대상에서 제외 (이중 파라미터화 방지)
165
- self.adapter_exclude_layers = list(adapter_exclude_layers or ())
166
- self.lambda_traj = lambda_traj
167
- # v2 anti-collapse switches (defaults False so v1 checkpoints reload).
168
- # read_shortcut: r_theta adds an orthogonally-projected pooled-V* term to
169
- # every slot, so Z^(0) is image-dependent from step 0 instead of relying
170
- # on a random cross-attention finding signal.
171
- # splice_scale_match: P_Z(Z) rows are RMS-matched to the question rows at
172
- # the splice. Unmatched, slot states are ~O(1) against layer-20 hiddens
173
- # in the hundreds -- the decoder could ignore them for free, and did
174
- # (probe 20143: visual sensitivity 0.94 -> 1.0000 over training).
175
- self.read_shortcut = read_shortcut
176
- self.splice_scale_match = splice_scale_match
177
- # v6 candidate: gated re-reads DURING the recurrence. Every re-read
178
- # goes through r_theta into Z, so the mediation invariant (decoder sees
179
- # only [Q*; P_Z(Z)]) is untouched; what changes is that the workspace
180
- # may consult V* again mid-reasoning, scaled by a learned scalar gate.
181
- # The gate is free at EVERY step -- no hand-coded schedule. Degeneration
182
- # into answer-time retrieval is disciplined by the objective itself:
183
- # L_traj pins each intermediate state to its teacher segment, so a
184
- # wait-then-fetch trajectory misses its intermediate targets. The gate
185
- # bias initialises negative, i.e. training starts from the proven
186
- # no-reread regime and departs only where gradients demand it.
187
- # Default False: v4/v5 checkpoints reload bit-identically.
188
- self.read_gating = read_gating
189
- # Initial gate open-rate epsilon_gamma: b_gamma = logit(eps). Sigmoid
190
- # never reaches exactly 0 at finite logits, so this is near-identity
191
- # (not bitwise) initialisation w.r.t. the read-once path.
192
- self.gate_init_eps = gate_init_eps
193
- # v7: always-on PURE visual residual rereads. Every step adds
194
- # Z = U + W_o XAttn(LN(U) -> queries, V* -> keys/values): no self-attn,
195
- # no FFN, no output norm, no biases in the branch, W_o zero-init.
196
- # Motivation (battery, ep1 ckpt 20752): read_once hurt badly while
197
- # null_step (blank memory at one step) was harmless -- the gated
198
- # reader's value was extra recurrent DEPTH, not vision. This branch
199
- # removes every path that can change Z without V* content, so any
200
- # benefit it shows IS visual by construction.
201
- self.read_residual_pure = read_residual_pure
202
- # v7.1 (advisor): relative-norm trust region on the visual residual.
203
- # s_k = min(1, rho * RMS(U) / (RMS(Delta) + eps)); Z = U + s_k * Delta
204
- # Unlike a fixed alpha, the optimizer cannot undo this by rescaling
205
- # W_o (s_k adapts); ||Delta_used|| <~ rho * ||U|| structurally, and
206
- # V* = 0 => Delta = 0 keeps the zero-memory identity. 0.0 disables.
207
- self.residual_norm_cap = residual_norm_cap
208
- # v7.1: initialise pure_attn K/V projections from the initial visual
209
- # reader's v_in (same [d_w, d] shape) and Q from its first read
210
- # block's in-proj slice, instead of random xavier -- the ep1 finding
211
- # was that Q/K/V stayed ~random while only W_o trained.
212
- self.pure_warm_start = pure_warm_start
213
- # MAIN METHOD (advisor-confirmed): Final-Verified Recurrent Workspace.
214
- # Initial read -> PURE latent recurrence (no mid-step visual anything)
215
- # -> exactly ONE pure visual verification at the end:
216
- # Z_out = U^(T) + W_o XAttn(LN(U^(T)), V*), alpha = 1, W_o = 0 init.
217
- # Uses the same bias-free verifier module as read_residual_pure; the
218
- # two flags differ only in WHERE the branch fires (every step vs once).
219
- self.final_verify = final_verify
220
- # MAIN (advisor 최종 확정): Mid-Read Recurrent Workspace. 고정 상수
221
- # k* = floor(T/2) = 4 (스텝 sweep 산물이 아니라 판독 전후 동수 전이의
222
- # midpoint)에서 pure visual read 1회:
223
- # Z^(4) = U^(4) + W_o XAttn(LN(U^(4)), V*), alpha=1, W_o=0 init.
224
- # 논리적 step 4 == python 루프 index 3 (0-based) -- off-by-one은
225
- # tests/test_mid_read.py가 상태 분기 위치로 고정한다. 0이면 비활성.
226
- self.mid_read_step = mid_read_step
227
- # advisor(2026-08-06): E_slot(=workspace_read.slots)을 매 recurrent
228
- # step 입력과 mid-read query에 일시 제공. state에는 누적하지 않는다.
229
- self.persistent_slot_id = persistent_slot_id
230
- # 고정 read 스케일 Z = U + alpha*W_o A (advisor scale-mismatch 검사).
231
- # 1.0 = 기존과 비트동일. eval의 gate_overrides alpha는 절대값으로 이를 대체.
232
- self.mid_read_alpha = mid_read_alpha
233
- # advisor 최종안: E(persistent evidence) / R(recurrent reasoning) 분리.
234
- # states = [E^(0), R^(1..T)], 결합 Z^(T)=E+R^(T)는 prefill에서.
235
- self.evidence_reasoning = evidence_reasoning
236
- # Current replacement candidate: no learned evidence/reasoning slots.
237
- # E is the complete raw V* sequence and R0 is the text-only Q* sequence.
238
- # A single shared CrossAttn transition produces R1..RT. Residual mode
239
- # decodes RT; the replacement candidate decodes Q*+HT. Cross-attention
240
- # uses the backbone's native GQA shape and
241
- # is initialized from the first upper layer by default. Checkpoints
242
- # created before the CrossAttn-only decision did not serialize
243
- # raw_transition_ffn; None therefore preserves their historical FFN
244
- # for faithful evaluation, while every new training launch passes False.
245
- self.raw_evidence_question = bool(raw_evidence_question)
246
- self.raw_attention_source_layer = int(raw_attention_source_layer)
247
- self.raw_transition_ffn = (
248
- True if raw_transition_ffn is None else bool(raw_transition_ffn)
249
- )
250
- # Experimental simplification of the same raw-E/Q path. The residual
251
- # carrier is replaced, not augmented:
252
- # H0=Q*, Hk=CrossAttn(H{k-1}, E), decoder_latent=Q*+HT.
253
- # False preserves all residual raw-E/Q checkpoints and active runs.
254
- self.raw_state_replacement = bool(raw_state_replacement)
255
- # Question-anchored replacement keeps the fixed text scaffold in every
256
- # recurrent query without restoring the old recurrent identity path:
257
- # Z0=Q*, Zk=Q*+CrossAttn(Z{k-1}, E), decoder_latent=ZT.
258
- # This replaces the one-time Q*+HT decoder interface above.
259
- self.raw_question_anchor = bool(raw_question_anchor)
260
- # Q-shaped recurrent candidate selected by the V* interface oracle:
261
- # R0=Q*, Rk=R{k-1}+CrossAttn(R{k-1}, V*)
262
- # R_agg=mean(R1..RT)
263
- # This is parameter-free and replaces both question-anchored state
264
- # replacement and final-state-only decoding.
265
- self.raw_mean_aggregation = bool(raw_mean_aggregation)
266
- # Counterfactual Residual Recurrence (CRR): the image-conditioned and
267
- # text-only question states are propagated by the same native layer
268
- # F=L(ell_star+1). Only their counterfactual residual is recurrently
269
- # refined; there are no slots, V* memory, custom attention modules, or
270
- # inference-time auxiliary modules. beta is the fixed relaxed-update
271
- # coefficient; an optional batch-swap reliance loss is training-only.
272
- self.counterfactual_residual_recurrence = bool(
273
- counterfactual_residual_recurrence
274
- )
275
- # Qwen3 counterpart of the promoted Qwen2.5 full-state CRR path. The
276
- # multimodal scaffold is used only inside the shared recurrent cell;
277
- # answer decoding still receives a question-shaped state and a
278
- # text-only prefix cache.
279
- self.visual_counterfactual_recurrence = bool(
280
- visual_counterfactual_recurrence
281
- )
282
- self.visual_cumulative_recurrence = bool(
283
- visual_cumulative_recurrence
284
- )
285
- self.visual_full_state_recurrence = bool(
286
- visual_full_state_recurrence
287
- )
288
- self.recurrence_only_adapter = bool(recurrence_only_adapter)
289
- self.counterfactual_beta = float(counterfactual_beta)
290
- # Bias-free Concat(C1..CT) -> C_agg, initialized to the exact
291
- # historical mean interface for checkpoint-compatible warm starts.
292
- self.crr_concat_aggregation = bool(crr_concat_aggregation)
293
- # Optional latent-only policy adapter. It corrects the deterministic
294
- # C2..CT means but never touches B, C1, or answer-token decoding. Rank 0
295
- # is the exact historical CRR path; RL checkpoints serialize rank > 0.
296
- self.crr_policy_rank = int(crr_policy_rank)
297
- self.crr_policy_alpha = float(crr_policy_alpha)
298
- # Training-only CRR intervention. A batch-matched residual from a
299
- # different-answer example replaces the factual residual before the
300
- # upper decoder. The model returns the factual-minus-swapped answer
301
- # score gap; the Trainer applies the margin loss. Evaluation and
302
- # generation never execute the extra branch.
303
- self.counterfactual_reliance_loss = bool(
304
- counterfactual_reliance_loss
305
- )
306
- self.transition_aware_reliance_loss = bool(
307
- transition_aware_reliance_loss
308
- )
309
- # Training-only functional no-regression objective. Gold-answer NLL
310
- # is measured through the actual upper decoder at every recurrent CRR
311
- # prefix; inference remains the unchanged T-step mean interface.
312
- self.counterfactual_step_retention_loss = bool(
313
- counterfactual_step_retention_loss
314
- )
315
- # Training-only self-distillation target. The multimodal lower pass
316
- # already computed for V* also provides its image-conditioned question
317
- # rows. They supervise R_agg at the split boundary, but are never
318
- # exposed to the inference decoder.
319
- self.functional_state_loss = bool(functional_state_loss)
320
- # Token CE lets long caption answers dominate one-token visual-choice
321
- # examples. The per-example variant first averages valid answer tokens
322
- # within each sample, then averages samples. It changes training/eval
323
- # loss reduction only and has no inference-time effect.
324
- self.per_example_answer_loss = bool(per_example_answer_loss)
325
- # ER v2 (advisor 2026-08-11): explicit evidence read —
326
- # R^(k) = f_theta(R^(k-1), Q*; E), E는 별도 memory로 cross-attn 읽기.
327
- # (E+R 합산-감산 제거. evidence_reasoning=True 전제.)
328
- self.explicit_evidence_read = bool(kwargs.pop("explicit_evidence_read", False))
329
- # Historical field name retained for checkpoint compatibility. In ER
330
- # v2.2 this means R^(0) is a batch-broadcast set of learnable slots; it
331
- # is deliberately NOT initialized from Q*. Q* conditions every shared
332
- # transition instead.
333
- self.r_init_question = bool(kwargs.pop("r_init_question", False))
334
- # ER v2.1 (advisor 2026-08-11): evidence read의 competitive normalization —
335
- # slot 축 softmax 후 evidence 축 재정규화 (Slot Attention식 경쟁).
336
- # 파라미터/모듈/loss 무추가; evidence_read의 W_qkv/W_o 재사용.
337
- self.competitive_evidence_read = bool(
338
- kwargs.pop("competitive_evidence_read", False))
339
- # ER v2.2 C: k>=1 traj 감독 부재 시 twin 체인 제거 (states = answer 체인).
340
- self.er_single_chain = bool(kwargs.pop("er_single_chain", False))
341
- # ER v2.2 C (§5-7): decoder latent = softmax(temporal_logits)로 가중한
342
- # R1..RT 결합 (전역 스칼라 T개). er_single_chain 전제. (legacy)
343
- self.temporal_aggregation = bool(kwargs.pop("temporal_aggregation", False))
344
- # ER v2.2 C 확정판: R_agg = Linear(Concat(R1..RT), bias=False),
345
- # 초기 W=[I/T,...,I/T] (R_agg=mean(R1..RT)). er_single_chain 전제.
346
- self.concat_aggregation = bool(kwargs.pop("concat_aggregation", False))
347
- # ER v2: 워크스페이스 블록의 rank-r 부분공간 attention/FFN (0 = dense).
348
- # d_w=d에서 state width 유지와 파라미터 폭증을 분리 (advisor).
349
- self.workspace_low_rank = int(kwargs.pop("workspace_low_rank", 0))
350
- # advisor: single-layer text-side interface alignment (bridge-only).
351
- # teacher = frozen-base mm 경로 q_end@l*+1 (adapter off, sg);
352
- # student = detach(R^T) splice의 l*+1 한 층 재계산. 추론 시 무존재.
353
- self.interface_loss = interface_loss
354
- # Training-only visual anchor switch. False in the current answer-only
355
- # method; when false the model does not even materialize pooled V* for
356
- # the Trainer.
357
- self.read_anchor = bool(read_anchor)
358
- if evidence_reasoning and persistent_slot_id:
359
- raise ValueError("evidence_reasoning은 persistent_slot_id와 동시 사용 불가")
360
- if evidence_reasoning and not mid_read_step:
361
- raise ValueError("evidence_reasoning은 mid_read_step > 0 필요")
362
- self.validate()
363
-
364
- # -- derived views -----------------------------------------------------
365
-
366
- @property
367
- def num_decoder_layers(self) -> int:
368
- return self.text_config.num_hidden_layers
369
-
370
- @property
371
- def backbone_width(self) -> int:
372
- """``d`` -- backbone hidden width (4096 for the 7B)."""
373
- return self.text_config.hidden_size
374
-
375
- @property
376
- def lower_slice(self) -> slice:
377
- """Layers forming ``F_{<=l*}``."""
378
- self._require_ell_star()
379
- return slice(0, self.ell_star + 1)
380
-
381
- @property
382
- def upper_slice(self) -> slice:
383
- """Layers forming ``F_{>l*}``."""
384
- self._require_ell_star()
385
- return slice(self.ell_star + 1, self.num_decoder_layers)
386
-
387
- # -- validation --------------------------------------------------------
388
-
389
- def _require_ell_star(self) -> None:
390
- if self.ell_star is None:
391
- raise ValueError(
392
- "ell_star is unset. Run scripts/localize_read_point.py (Method 3.1) "
393
- "and pass the measured layer explicitly; it must not be guessed."
394
- )
395
-
396
- def validate(self) -> None:
397
- """Reject configurations that silently break the §3.2 contract."""
398
- if self.ell_star is not None:
399
- n = self.num_decoder_layers
400
- # Both branches must be non-empty: an empty lower branch means there
401
- # is no V* to read, an empty upper branch means the workspace never
402
- # reaches the decoder.
403
- if not 0 <= self.ell_star <= n - 2:
404
- raise ValueError(
405
- f"ell_star={self.ell_star} out of range for {n} decoder layers; "
406
- f"expected 0 <= ell_star <= {n - 2} so both branches are non-empty."
407
- )
408
- # K=0 is the no-recurrence control (read only), not a misconfiguration.
409
- if self.num_workspace_steps < 0:
410
- raise ValueError("num_workspace_steps (K) must be >= 0.")
411
- if self.num_workspace_slots < 1:
412
- raise ValueError("num_workspace_slots (S) must be >= 1.")
413
- if self.workspace_num_heads < 1:
414
- raise ValueError("workspace_num_heads must be >= 1.")
415
- if self.workspace_width % self.workspace_num_heads != 0:
416
- raise ValueError(
417
- f"workspace_width={self.workspace_width} must be divisible by "
418
- f"workspace_num_heads={self.workspace_num_heads}."
419
- )
420
- if self.workspace_width > self.backbone_width:
421
- raise ValueError(
422
- f"workspace_width (d_w={self.workspace_width}) cannot exceed "
423
- f"backbone width (d={self.backbone_width}). Native-width d_w=d is "
424
- "the current method and uses an Identity decoder interface."
425
- )
426
- if self.workspace_rope_mode not in WORKSPACE_ROPE_MODES:
427
- raise ValueError(
428
- f"workspace_rope_mode={self.workspace_rope_mode!r} not in "
429
- f"{WORKSPACE_ROPE_MODES}."
430
- )
431
- if self.adapter_rank < 0:
432
- raise ValueError("adapter_rank must be >= 0 (0 disables the adapter).")
433
- if not math.isfinite(self.adapter_dropout) or not 0.0 <= self.adapter_dropout < 1.0:
434
- raise ValueError("adapter_dropout must be finite and in [0, 1).")
435
- if getattr(self, "crr_policy_rank", 0) < 0:
436
- raise ValueError("crr_policy_rank must be >= 0.")
437
- if not math.isfinite(getattr(self, "crr_policy_alpha", 0.0)) or getattr(
438
- self, "crr_policy_alpha", 0.0
439
- ) <= 0.0:
440
- raise ValueError("crr_policy_alpha must be finite and > 0.")
441
- if self.read_num_blocks < 1 or self.transition_num_blocks < 1:
442
- raise ValueError("read_num_blocks and transition_num_blocks must be >= 1.")
443
- if self.workspace_ffn_mult < 1:
444
- raise ValueError("workspace_ffn_mult must be >= 1.")
445
- mid = int(getattr(self, "mid_read_step", 0) or 0)
446
- if mid < 0 or mid > self.num_workspace_steps:
447
- raise ValueError(
448
- f"mid_read_step={mid} must be in [0, num_workspace_steps="
449
- f"{self.num_workspace_steps}]."
450
- )
451
- low_rank = int(getattr(self, "workspace_low_rank", 0) or 0)
452
- if low_rank < 0:
453
- raise ValueError("workspace_low_rank must be >= 0.")
454
- if low_rank and low_rank % self.workspace_num_heads != 0:
455
- raise ValueError(
456
- f"workspace_low_rank={low_rank} must be divisible by "
457
- f"workspace_num_heads={self.workspace_num_heads}."
458
- )
459
- if getattr(self, "competitive_evidence_read", False) and not getattr(
460
- self, "explicit_evidence_read", False
461
- ):
462
- raise ValueError(
463
- "competitive_evidence_read requires explicit_evidence_read=True; "
464
- "otherwise the flag has no execution path."
465
- )
466
- if getattr(self, "explicit_evidence_read", False) and not getattr(
467
- self, "evidence_reasoning", False
468
- ):
469
- raise ValueError(
470
- "explicit_evidence_read requires evidence_reasoning=True; otherwise "
471
- "the evidence-read module is allocated but never called."
472
- )
473
- if getattr(self, "r_init_question", False) and not getattr(
474
- self, "evidence_reasoning", False
475
- ):
476
- raise ValueError(
477
- "r_init_question (legacy name for learnable R0 slots) requires "
478
- "evidence_reasoning=True; otherwise reasoning_slots are unused."
479
- )
480
- if (getattr(self, "concat_aggregation", False)
481
- or getattr(self, "temporal_aggregation", False)):
482
- if self.num_workspace_steps < 1:
483
- raise ValueError("reasoning aggregation requires num_workspace_steps >= 1.")
484
- if not getattr(self, "evidence_reasoning", False):
485
- raise ValueError(
486
- "reasoning aggregation requires evidence_reasoning=True; "
487
- "otherwise the aggregation module is unused."
488
- )
489
- if getattr(self, "concat_aggregation", False) and not getattr(
490
- self, "er_single_chain", False
491
- ):
492
- raise ValueError("concat_aggregation requires er_single_chain=True.")
493
- if getattr(self, "raw_evidence_question", False):
494
- if self.workspace_width != self.backbone_width:
495
- raise ValueError(
496
- "raw_evidence_question requires native workspace_width == "
497
- "backbone_width: E=V* and R0=Q* are used without projections."
498
- )
499
- if self.num_workspace_steps < 1:
500
- raise ValueError(
501
- "raw_evidence_question requires num_workspace_steps >= 1."
502
- )
503
- source_layer = (
504
- self.ell_star + 1
505
- if self.raw_attention_source_layer < 0
506
- else self.raw_attention_source_layer
507
- )
508
- if not self.ell_star < source_layer < self.text_config.num_hidden_layers:
509
- raise ValueError(
510
- "raw_attention_source_layer must be an upper-backbone layer, "
511
- f"got ell_star={self.ell_star}, source={source_layer}."
512
- )
513
- incompatible = {
514
- "evidence_reasoning": bool(getattr(self, "evidence_reasoning", False)),
515
- "explicit_evidence_read": bool(getattr(self, "explicit_evidence_read", False)),
516
- "competitive_evidence_read": bool(getattr(self, "competitive_evidence_read", False)),
517
- "r_init_question": bool(getattr(self, "r_init_question", False)),
518
- "er_single_chain": bool(getattr(self, "er_single_chain", False)),
519
- "temporal_aggregation": bool(getattr(self, "temporal_aggregation", False)),
520
- "concat_aggregation": bool(getattr(self, "concat_aggregation", False)),
521
- "read_shortcut": bool(getattr(self, "read_shortcut", False)),
522
- "splice_scale_match": bool(getattr(self, "splice_scale_match", False)),
523
- "read_gating": bool(getattr(self, "read_gating", False)),
524
- "read_residual_pure": bool(getattr(self, "read_residual_pure", False)),
525
- "final_verify": bool(getattr(self, "final_verify", False)),
526
- "mid_read_step": bool(getattr(self, "mid_read_step", 0)),
527
- "persistent_slot_id": bool(getattr(self, "persistent_slot_id", False)),
528
- "interface_loss": bool(getattr(self, "interface_loss", False)),
529
- "read_anchor": bool(getattr(self, "read_anchor", False)),
530
- }
531
- active = [name for name, enabled in incompatible.items() if enabled]
532
- if active:
533
- raise ValueError(
534
- "raw_evidence_question replaces the slot workspace and is "
535
- f"incompatible with: {', '.join(active)}"
536
- )
537
- if self.raw_state_replacement and self.raw_transition_ffn:
538
- raise ValueError(
539
- "raw_state_replacement is CrossAttn-only and is incompatible "
540
- "with raw_transition_ffn=True."
541
- )
542
- if self.raw_question_anchor and not self.raw_state_replacement:
543
- raise ValueError(
544
- "raw_question_anchor requires raw_state_replacement=True."
545
- )
546
- if self.raw_mean_aggregation and (
547
- self.raw_state_replacement or self.raw_question_anchor
548
- ):
549
- raise ValueError(
550
- "raw_mean_aggregation replaces raw_state_replacement and "
551
- "raw_question_anchor; use the residual recurrence."
552
- )
553
- if self.functional_state_loss and not self.raw_mean_aggregation:
554
- raise ValueError(
555
- "functional_state_loss requires raw_mean_aggregation=True "
556
- "so its target is the exact decoder-facing recurrence."
557
- )
558
- elif getattr(self, "raw_state_replacement", False):
559
- raise ValueError(
560
- "raw_state_replacement requires raw_evidence_question=True."
561
- )
562
- elif getattr(self, "raw_question_anchor", False):
563
- raise ValueError(
564
- "raw_question_anchor requires raw_evidence_question=True."
565
- )
566
- elif getattr(self, "raw_mean_aggregation", False):
567
- raise ValueError(
568
- "raw_mean_aggregation requires raw_evidence_question=True."
569
- )
570
- elif getattr(self, "functional_state_loss", False):
571
- raise ValueError(
572
- "functional_state_loss requires raw_evidence_question=True."
573
- )
574
- if (
575
- getattr(self, "counterfactual_reliance_loss", False)
576
- and not getattr(self, "counterfactual_residual_recurrence", False)
577
- ):
578
- raise ValueError(
579
- "counterfactual_reliance_loss requires "
580
- "counterfactual_residual_recurrence=True."
581
- )
582
- if (
583
- getattr(self, "transition_aware_reliance_loss", False)
584
- and not getattr(self, "counterfactual_reliance_loss", False)
585
- ):
586
- raise ValueError(
587
- "transition_aware_reliance_loss requires "
588
- "counterfactual_reliance_loss=True."
589
- )
590
- if (
591
- getattr(self, "transition_aware_reliance_loss", False)
592
- and self.num_workspace_steps < 2
593
- ):
594
- raise ValueError(
595
- "transition_aware_reliance_loss requires at least two steps."
596
- )
597
- if (
598
- getattr(self, "counterfactual_step_retention_loss", False)
599
- and not getattr(self, "counterfactual_residual_recurrence", False)
600
- ):
601
- raise ValueError(
602
- "counterfactual_step_retention_loss requires "
603
- "counterfactual_residual_recurrence=True."
604
- )
605
- if (
606
- getattr(self, "counterfactual_step_retention_loss", False)
607
- and self.num_workspace_steps < 2
608
- ):
609
- raise ValueError(
610
- "counterfactual_step_retention_loss requires at least two steps."
611
- )
612
- if getattr(self, "crr_concat_aggregation", False) and not getattr(
613
- self, "counterfactual_residual_recurrence", False
614
- ):
615
- raise ValueError(
616
- "crr_concat_aggregation requires "
617
- "counterfactual_residual_recurrence=True."
618
- )
619
- if getattr(self, "counterfactual_residual_recurrence", False):
620
- if getattr(self, "raw_evidence_question", False):
621
- raise ValueError(
622
- "counterfactual_residual_recurrence replaces "
623
- "raw_evidence_question; enable exactly one q-state method."
624
- )
625
- if self.num_workspace_steps < 1:
626
- raise ValueError(
627
- "counterfactual_residual_recurrence requires "
628
- "num_workspace_steps >= 1."
629
- )
630
- if self.crr_policy_rank > 0 and self.num_workspace_steps < 2:
631
- raise ValueError(
632
- "crr_policy_rank > 0 requires num_workspace_steps >= 2."
633
- )
634
- if not 0.0 <= self.counterfactual_beta <= 1.0:
635
- raise ValueError(
636
- "counterfactual_beta must lie in [0, 1], got "
637
- f"{self.counterfactual_beta}."
638
- )
639
- incompatible = {
640
- "evidence_reasoning": bool(getattr(self, "evidence_reasoning", False)),
641
- "explicit_evidence_read": bool(getattr(self, "explicit_evidence_read", False)),
642
- "competitive_evidence_read": bool(getattr(self, "competitive_evidence_read", False)),
643
- "r_init_question": bool(getattr(self, "r_init_question", False)),
644
- "er_single_chain": bool(getattr(self, "er_single_chain", False)),
645
- "temporal_aggregation": bool(getattr(self, "temporal_aggregation", False)),
646
- "concat_aggregation": bool(getattr(self, "concat_aggregation", False)),
647
- "read_shortcut": bool(getattr(self, "read_shortcut", False)),
648
- "splice_scale_match": bool(getattr(self, "splice_scale_match", False)),
649
- "read_gating": bool(getattr(self, "read_gating", False)),
650
- "read_residual_pure": bool(getattr(self, "read_residual_pure", False)),
651
- "final_verify": bool(getattr(self, "final_verify", False)),
652
- "mid_read_step": bool(getattr(self, "mid_read_step", 0)),
653
- "persistent_slot_id": bool(getattr(self, "persistent_slot_id", False)),
654
- "interface_loss": bool(getattr(self, "interface_loss", False)),
655
- "read_anchor": bool(getattr(self, "read_anchor", False)),
656
- "functional_state_loss": bool(getattr(self, "functional_state_loss", False)),
657
- }
658
- active = [name for name, enabled in incompatible.items() if enabled]
659
- if active:
660
- raise ValueError(
661
- "counterfactual_residual_recurrence replaces the slot/raw "
662
- f"workspace and is incompatible with: {', '.join(active)}"
663
- )
664
- elif getattr(self, "crr_policy_rank", 0) > 0:
665
- raise ValueError(
666
- "crr_policy_rank > 0 requires "
667
- "counterfactual_residual_recurrence=True."
668
- )
669
- if getattr(self, "visual_counterfactual_recurrence", False):
670
- if not getattr(self, "counterfactual_residual_recurrence", False):
671
- raise ValueError(
672
- "visual_counterfactual_recurrence requires "
673
- "counterfactual_residual_recurrence=True."
674
- )
675
- if not getattr(self, "visual_cumulative_recurrence", False):
676
- raise ValueError(
677
- "Qwen3 visual recurrence currently requires the promoted "
678
- "cumulative full-state path."
679
- )
680
- if not getattr(self, "visual_full_state_recurrence", False):
681
- raise ValueError(
682
- "Qwen3 visual recurrence currently requires "
683
- "visual_full_state_recurrence=True."
684
- )
685
- incompatible = {
686
- "counterfactual_reliance_loss": bool(
687
- getattr(self, "counterfactual_reliance_loss", False)
688
- ),
689
- "transition_aware_reliance_loss": bool(
690
- getattr(self, "transition_aware_reliance_loss", False)
691
- ),
692
- "counterfactual_step_retention_loss": bool(
693
- getattr(self, "counterfactual_step_retention_loss", False)
694
- ),
695
- "crr_concat_aggregation": bool(
696
- getattr(self, "crr_concat_aggregation", False)
697
- ),
698
- "crr_policy_rank": bool(getattr(self, "crr_policy_rank", 0)),
699
- }
700
- active = [name for name, enabled in incompatible.items() if enabled]
701
- if active:
702
- raise ValueError(
703
- "Qwen3 full-state visual recurrence uses final-answer CE "
704
- "and final-state decoding only; incompatible with: "
705
- + ", ".join(active)
706
- )
707
- elif getattr(self, "visual_cumulative_recurrence", False) or getattr(
708
- self, "visual_full_state_recurrence", False
709
- ):
710
- raise ValueError(
711
- "visual cumulative/full-state flags require "
712
- "visual_counterfactual_recurrence=True."
713
- )
714
- if getattr(self, "recurrence_only_adapter", False):
715
- if not getattr(self, "visual_full_state_recurrence", False):
716
- raise ValueError(
717
- "recurrence_only_adapter requires "
718
- "visual_full_state_recurrence=True."
719
- )
720
- if self.adapter_rank <= 0:
721
- raise ValueError(
722
- "recurrence_only_adapter requires adapter_rank > 0."
723
- )
724
- recurrent_layer = int(self.ell_star) + 1
725
- if recurrent_layer in set(self.adapter_exclude_layers):
726
- raise ValueError(
727
- "recurrence_only_adapter cannot exclude its recurrent "
728
- f"layer {recurrent_layer} from LoRA."
729
- )
730
- modes = [bool(getattr(self, m, False)) for m in
731
- ("read_gating", "read_residual_pure", "final_verify")]
732
- modes.append(bool(getattr(self, "mid_read_step", 0)))
733
- if sum(modes) > 1:
734
- raise ValueError(
735
- "read_gating / read_residual_pure / final_verify are mutually "
736
- "exclusive reread modes."
737
- )
738
-
739
-
740
- __all__ = ["CloseQwen3VLConfig", "WORKSPACE_ROPE_MODES"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cvrr_release_config.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "name": "CVRR-InternVL3-9B",
3
- "base_model": "OpenGVLab/InternVL3-9B",
4
- "ell_star": 35,
5
- "recurrent_layer": 36,
6
- "upper_decoder_start": 37,
7
- "inference_T": 4,
8
- "inference_beta": 0.33,
9
- "lora_rank": 32,
10
- "lora_alpha": 12.0,
11
- "lora_dropout": 0.01,
12
- "checkpoint_step": 500,
13
- "format": "cvrr_native_plus_merged_transition_v1",
14
- "merged_weight_dtype": "float32",
15
- "native_weight_bytes": 18277586944,
16
- "status": "gpu_vstar_comparison_completed",
17
- "upload_ready": true,
18
- "hub_model_id": "dmis-lab/InternVL3-9B-CVRR"
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modeling_cvrr_merged.py CHANGED
@@ -6,7 +6,6 @@ tensors or external backbone download is required after loading the package.
6
  from __future__ import annotations
7
 
8
  import contextlib
9
- import json
10
  import types
11
  from pathlib import Path
12
  from typing import TYPE_CHECKING
@@ -21,16 +20,16 @@ from .configuration_cvrr_merged import CVRRMergedConfig
21
  # Transformers 4.57's local-directory loader copies direct relative imports;
22
  # enumerate transitive helpers without eagerly importing every backbone.
23
  if TYPE_CHECKING:
24
- from .source_splitting import SplitContext
25
- from .source_perceive import PerceiveDeliberateLayout
26
- from .source_spatial import SpatialVisualRecurrentCell
27
  from .source_helpers import _layer_hidden
28
  from .source_gemma import GemmaCVRR
29
  from .source_internvl import InternVLCVRR
30
- from .configuration_source_qwen25 import CloseQwen2_5_VLConfig
31
- from .configuration_source_qwen3 import CloseQwen3VLConfig
32
- from .modeling_source_qwen25 import CloseQwen2_5_VLForConditionalGeneration
33
- from .modeling_source_qwen3 import CloseQwen3VLForConditionalGeneration
34
 
35
 
36
  class NativeMergedLinear(nn.Module):
@@ -106,6 +105,108 @@ def install_merged(runtime, weights, *, generic):
106
  return runtime.eval()
107
 
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  class CVRRMergedModel(PreTrainedModel):
110
  config_class = CVRRMergedConfig
111
  main_input_name = 'input_ids'
@@ -147,17 +248,16 @@ class CVRRMergedModel(PreTrainedModel):
147
  token=token, local_files_only=offline))
148
  if config is None:
149
  config = CVRRMergedConfig.from_pretrained(path, local_files_only=True)
150
- settings = json.loads((path / 'cvrr_release_config.json').read_text())
151
- if config.release != settings:
152
- raise ValueError('Root configuration and release metadata disagree')
153
  if settings['format'] != 'cvrr_native_plus_merged_transition_v1':
154
  raise ValueError('Unsupported release layout')
155
  native_path = str(path / 'native_backbone')
156
  generic = 'Qwen' not in settings['name']
 
 
 
157
  common = dict(ell_star=settings['ell_star'], steps=settings['inference_T'],
158
- beta=settings['inference_beta'], rank=settings['lora_rank'],
159
- alpha=settings['lora_alpha'], dropout=settings['lora_dropout'],
160
- device=device, offline=True)
161
  if generic:
162
  if 'InternVL' in settings['name']:
163
  from .source_internvl import InternVLCVRR
@@ -168,12 +268,12 @@ class CVRRMergedModel(PreTrainedModel):
168
  else:
169
  if 'Qwen2.5' in settings['name']:
170
  from transformers import Qwen2_5_VLForConditionalGeneration as Native
171
- from .configuration_source_qwen25 import CloseQwen2_5_VLConfig as SourceConfig
172
- from .modeling_source_qwen25 import CloseQwen2_5_VLForConditionalGeneration as Source
173
  else:
174
  from transformers import Qwen3VLForConditionalGeneration as Native
175
- from .configuration_source_qwen3 import CloseQwen3VLConfig as SourceConfig
176
- from .modeling_source_qwen3 import CloseQwen3VLForConditionalGeneration as Source
177
  source_config = SourceConfig.from_pretrained(path / 'source_config', local_files_only=True)
178
  source_config.num_workspace_steps = settings['inference_T']
179
  source_config.counterfactual_beta = settings['inference_beta']
@@ -272,7 +372,25 @@ class CVRRMergedModel(PreTrainedModel):
272
  if do_sample:
273
  raise ValueError('Only deterministic greedy decoding is supported by this release')
274
  if self.is_generic:
275
- raise NotImplementedError('Gemma/InternVL release validation currently covers strict next-token readout only')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  return self.runtime.generate_answer(**inputs, max_new_tokens=max_new_tokens)
277
 
278
 
 
6
  from __future__ import annotations
7
 
8
  import contextlib
 
9
  import types
10
  from pathlib import Path
11
  from typing import TYPE_CHECKING
 
20
  # Transformers 4.57's local-directory loader copies direct relative imports;
21
  # enumerate transitive helpers without eagerly importing every backbone.
22
  if TYPE_CHECKING:
23
+ raise RuntimeError("This release does not bundle source_splitting")
24
+ raise RuntimeError("This release does not bundle source_perceive")
25
+ raise RuntimeError("This release does not bundle source_spatial")
26
  from .source_helpers import _layer_hidden
27
  from .source_gemma import GemmaCVRR
28
  from .source_internvl import InternVLCVRR
29
+ raise RuntimeError("This release does not bundle configuration_source_qwen25")
30
+ raise RuntimeError("This release does not bundle configuration_source_qwen3")
31
+ raise RuntimeError("This release does not bundle modeling_source_qwen25")
32
+ raise RuntimeError("This release does not bundle modeling_source_qwen3")
33
 
34
 
35
  class NativeMergedLinear(nn.Module):
 
105
  return runtime.eval()
106
 
107
 
108
+ def _generic_text_model(runtime):
109
+ return (runtime.base_model.language_model.model if runtime.model_type == 'internvl_chat'
110
+ else runtime.base_model.model)
111
+
112
+
113
+ def _generic_last_logits(runtime, hidden):
114
+ if runtime.model_type == 'internvl_chat':
115
+ logits = runtime.base_model.language_model.get_output_embeddings()(hidden[:, -1:])
116
+ else:
117
+ logits = runtime.base_model.lm_head(hidden[:, -1:])
118
+ if runtime.model_type in {'gemma4', 'gemma4_unified'}:
119
+ cap = runtime.base_model.config.text_config.final_logit_softcapping
120
+ if cap is not None:
121
+ logits = torch.tanh(logits / cap) * cap
122
+ return logits[:, -1].float()
123
+
124
+
125
+ def _replace_layer_hidden(output, hidden):
126
+ if hasattr(output, 'to_tuple'):
127
+ raise TypeError('decoder layer unexpectedly returned a ModelOutput')
128
+ if isinstance(output, tuple):
129
+ return (hidden, *output[1:])
130
+ if isinstance(output, list):
131
+ return [hidden, *output[1:]]
132
+ return hidden
133
+
134
+
135
+ def _generic_cache_length(cache):
136
+ """Read text length from either Transformers Cache or a legacy KV tuple."""
137
+ if hasattr(cache, 'get_seq_length'):
138
+ return int(cache.get_seq_length())
139
+ if not isinstance(cache, (tuple, list)) or not cache:
140
+ raise TypeError('strict generation received an unsupported KV cache')
141
+ lengths = []
142
+ for layer in cache:
143
+ if not isinstance(layer, (tuple, list)) or len(layer) < 2:
144
+ raise TypeError('strict generation received a malformed legacy KV cache')
145
+ for tensor in layer[:2]:
146
+ if not torch.is_tensor(tensor) or tensor.ndim < 3:
147
+ raise TypeError('strict generation received a malformed legacy KV tensor')
148
+ lengths.append(int(tensor.shape[-2]))
149
+ if len(set(lengths)) != 1:
150
+ raise RuntimeError('strict generation found inconsistent legacy KV lengths')
151
+ return lengths[0]
152
+
153
+
154
+ def _generic_strict_prefill(runtime, inputs, trace, recurrent):
155
+ """Cache only the image-free prompt, splicing R_T above the recurrent cell."""
156
+ if inputs['input_ids'].shape[0] != 1:
157
+ raise ValueError('strict generation currently supports one item at a time')
158
+ ids = inputs['input_ids'][trace.text_rows].unsqueeze(0)
159
+ called = []
160
+
161
+ def splice(_module, _args, output):
162
+ called.append(True)
163
+ return _replace_layer_hidden(output, recurrent)
164
+
165
+ hook = runtime.layers[runtime.cell_index].register_forward_hook(splice)
166
+ try:
167
+ with runtime.adapters(False):
168
+ result = _generic_text_model(runtime)(
169
+ input_ids=ids, attention_mask=torch.ones_like(ids),
170
+ use_cache=True, return_dict=True,
171
+ )
172
+ finally:
173
+ hook.remove()
174
+ if len(called) != 1 or result.past_key_values is None:
175
+ raise RuntimeError('strict text-only prefill did not produce one splice and a KV cache')
176
+ if _generic_cache_length(result.past_key_values) != ids.shape[1]:
177
+ raise RuntimeError('strict text-only KV cache length differs from the image-free prompt')
178
+ return _generic_last_logits(runtime, result.last_hidden_state), result.past_key_values
179
+
180
+
181
+ def _generic_greedy_tokens(runtime, logits, cache, question_length, *, max_new_tokens, eos_token_ids):
182
+ generated = []
183
+ with runtime.adapters(False):
184
+ for step in range(max_new_tokens):
185
+ if not bool(torch.isfinite(logits).all()):
186
+ raise FloatingPointError('non-finite greedy-generation logits')
187
+ token = logits.argmax(-1).reshape(1, 1)
188
+ generated.append(int(token.item()))
189
+ if generated[-1] in eos_token_ids or step + 1 == max_new_tokens:
190
+ break
191
+ mask = torch.ones((1, question_length + len(generated)), dtype=torch.long, device=token.device)
192
+ output = _generic_text_model(runtime)(
193
+ input_ids=token, attention_mask=mask,
194
+ past_key_values=cache, use_cache=True, return_dict=True,
195
+ )
196
+ cache = output.past_key_values
197
+ if _generic_cache_length(cache) != question_length + len(generated):
198
+ raise RuntimeError('strict text-only KV cache did not advance by one text token')
199
+ logits = _generic_last_logits(runtime, output.last_hidden_state)
200
+ return generated
201
+
202
+
203
+ def _add_eos_ids(target, value):
204
+ if value is None:
205
+ return
206
+ values = value if isinstance(value, (tuple, list, set)) else (value,)
207
+ target.update(int(token) for token in values)
208
+
209
+
210
  class CVRRMergedModel(PreTrainedModel):
211
  config_class = CVRRMergedConfig
212
  main_input_name = 'input_ids'
 
248
  token=token, local_files_only=offline))
249
  if config is None:
250
  config = CVRRMergedConfig.from_pretrained(path, local_files_only=True)
251
+ settings = config.release
 
 
252
  if settings['format'] != 'cvrr_native_plus_merged_transition_v1':
253
  raise ValueError('Unsupported release layout')
254
  native_path = str(path / 'native_backbone')
255
  generic = 'Qwen' not in settings['name']
256
+ # The generic constructors retain their internal LoRA defaults only to
257
+ # build temporary projection wrappers. install_merged() replaces those
258
+ # wrappers with the exported dense weights before inference.
259
  common = dict(ell_star=settings['ell_star'], steps=settings['inference_T'],
260
+ beta=settings['inference_beta'], device=device, offline=True)
 
 
261
  if generic:
262
  if 'InternVL' in settings['name']:
263
  from .source_internvl import InternVLCVRR
 
268
  else:
269
  if 'Qwen2.5' in settings['name']:
270
  from transformers import Qwen2_5_VLForConditionalGeneration as Native
271
+ raise RuntimeError("This release does not bundle configuration_source_qwen25")
272
+ raise RuntimeError("This release does not bundle modeling_source_qwen25")
273
  else:
274
  from transformers import Qwen3VLForConditionalGeneration as Native
275
+ raise RuntimeError("This release does not bundle configuration_source_qwen3")
276
+ raise RuntimeError("This release does not bundle modeling_source_qwen3")
277
  source_config = SourceConfig.from_pretrained(path / 'source_config', local_files_only=True)
278
  source_config.num_workspace_steps = settings['inference_T']
279
  source_config.counterfactual_beta = settings['inference_beta']
 
372
  if do_sample:
373
  raise ValueError('Only deterministic greedy decoding is supported by this release')
374
  if self.is_generic:
375
+ if max_new_tokens < 1:
376
+ raise ValueError('max_new_tokens must be positive')
377
+ explicit_eos = inputs.pop('eos_token_id', None)
378
+ tokenizer = getattr(self, 'tokenizer', getattr(self.runtime, 'tokenizer', None))
379
+ eos = set()
380
+ for source in (getattr(self.runtime.base_model, 'generation_config', None), tokenizer):
381
+ _add_eos_ids(eos, getattr(source, 'eos_token_id', None))
382
+ _add_eos_ids(eos, explicit_eos)
383
+ if self.runtime.model_type == 'internvl_chat':
384
+ sep = self.runtime.base_model.conv_template.sep.strip()
385
+ eos.add(int(self.runtime.tokenizer.convert_tokens_to_ids(sep)))
386
+ trace = self.runtime.extract(inputs)
387
+ recurrent = self.runtime.rollout(trace)[-1]
388
+ logits, cache = _generic_strict_prefill(self.runtime, inputs, trace, recurrent)
389
+ tokens = _generic_greedy_tokens(
390
+ self.runtime, logits, cache, int(trace.question_lengths[0].item()),
391
+ max_new_tokens=max_new_tokens, eos_token_ids=eos,
392
+ )
393
+ return torch.tensor([tokens], dtype=torch.long, device=logits.device)
394
  return self.runtime.generate_answer(**inputs, max_new_tokens=max_new_tokens)
395
 
396
 
modeling_source_qwen25.py DELETED
The diff for this file is too large to render. See raw diff
 
modeling_source_qwen3.py DELETED
The diff for this file is too large to render. See raw diff
 
source_perceive.py DELETED
@@ -1,414 +0,0 @@
1
- """Attention layouts for non-recurrent visual latent reasoning chains.
2
-
3
- The sequence is laid out as::
4
-
5
- [multimodal prompt ; clean question ; LOOK_1 ; THINK_1 ; ... ; answer]
6
-
7
- All rows are processed once by the native VLM decoder. A block-sparse causal
8
- graph makes LOOK rows the only latent rows with access to the multimodal
9
- prefix, while THINK rows integrate one visual read without directly seeing
10
- that prefix. Answer rows can see the clean question and THINK rows, but never
11
- the multimodal prefix or LOOK rows. Consequently the image-dependent answer
12
- path is
13
-
14
- image -> LOOK_k -> THINK_k -> answer,
15
-
16
- without a tied recurrent cell, a visual-state update, or an aggregation
17
- module.
18
-
19
- The transition-conditioned variant tightens the graph without adding a
20
- module. The first LOOK/THINK pair bootstraps from the complete prompt and the
21
- clean question. Every later pair sees the causal latent prefix, while its
22
- LOOK row receives only visual placeholder rows from the multimodal prefix.
23
- Thus later pairs cannot independently re-solve the original image/question
24
- prompt; their task semantics must arrive through earlier latent states. The
25
- last THINK is the sole latent exposed to answer rows and therefore acts as the
26
- native-transformer aggregation state.
27
- """
28
-
29
- from __future__ import annotations
30
-
31
- from dataclasses import dataclass
32
-
33
- import torch
34
-
35
-
36
- @dataclass(frozen=True)
37
- class PerceiveDeliberateLayout:
38
- """Physical row layout of one padded training/generation sequence."""
39
-
40
- multimodal_length: int
41
- question_length: int
42
- num_pairs: int
43
- answer_input_length: int = 0
44
-
45
- def __post_init__(self) -> None:
46
- if self.multimodal_length < 1:
47
- raise ValueError("multimodal_length must be positive")
48
- if self.question_length < 1:
49
- raise ValueError("question_length must be positive")
50
- if self.num_pairs < 1:
51
- raise ValueError("num_pairs must be positive")
52
- if self.answer_input_length < 0:
53
- raise ValueError("answer_input_length must be non-negative")
54
-
55
- @property
56
- def num_latent_tokens(self) -> int:
57
- return 2 * self.num_pairs
58
-
59
- @property
60
- def question_start(self) -> int:
61
- return self.multimodal_length
62
-
63
- @property
64
- def latent_start(self) -> int:
65
- return self.multimodal_length + self.question_length
66
-
67
- @property
68
- def answer_start(self) -> int:
69
- return self.latent_start + self.num_latent_tokens
70
-
71
- @property
72
- def sequence_length(self) -> int:
73
- return self.answer_start + self.answer_input_length
74
-
75
- @property
76
- def question_slice(self) -> slice:
77
- return slice(self.question_start, self.latent_start)
78
-
79
- @property
80
- def answer_slice(self) -> slice:
81
- return slice(self.answer_start, self.sequence_length)
82
-
83
- @property
84
- def look_indices(self) -> tuple[int, ...]:
85
- return tuple(self.latent_start + 2 * index for index in range(self.num_pairs))
86
-
87
- @property
88
- def think_indices(self) -> tuple[int, ...]:
89
- return tuple(index + 1 for index in self.look_indices)
90
-
91
-
92
- def build_perceive_deliberate_mask(
93
- layout: PerceiveDeliberateLayout,
94
- multimodal_attention_mask: torch.Tensor,
95
- question_attention_mask: torch.Tensor,
96
- answer_input_attention_mask: torch.Tensor | None = None,
97
- *,
98
- multimodal_visual_mask: torch.Tensor | None = None,
99
- local_visual_chain: bool = False,
100
- transition_conditioned: bool = False,
101
- question_visible_through_pair: int = 1,
102
- disable_chain_links: bool = False,
103
- disable_late_visual: bool = False,
104
- final_pair_only: bool = False,
105
- ) -> torch.BoolTensor:
106
- """Build the strict LOOK/THINK/answer visibility graph.
107
-
108
- The returned boolean mask has shape ``[B, 1, L, L]`` and follows PyTorch
109
- SDPA semantics: ``True`` means that the query may attend to the key.
110
-
111
- In the original graph, LOOK_k sees the valid multimodal prompt, the clean
112
- question, and (for k>1) only THINK_{k-1}; THINK_k sees the clean question
113
- and LOOK_k. In the transition-conditioned graph, pair 1 keeps those
114
- bootstrap inputs, but every later latent sees its complete causal latent
115
- prefix, later LOOK rows see only visual placeholder keys, and later THINK
116
- rows normally receive no independent question shortcut. For a controlled
117
- curriculum/intervention, ``question_visible_through_pair`` may temporarily
118
- retain the clean-question edge through a later pair; ``1`` is the strict
119
- inference graph. Answer rows see only the final THINK in that variant.
120
- Every row also sees itself so padded query
121
- rows remain numerically defined; padded rows are never exposed as keys to
122
- semantic rows.
123
- """
124
-
125
- if multimodal_attention_mask.ndim != 2:
126
- raise ValueError("multimodal_attention_mask must have shape [B, L_mm]")
127
- if question_attention_mask.ndim != 2:
128
- raise ValueError("question_attention_mask must have shape [B, L_q]")
129
- batch_size = multimodal_attention_mask.shape[0]
130
- expected_mm = (batch_size, layout.multimodal_length)
131
- expected_q = (batch_size, layout.question_length)
132
- if tuple(multimodal_attention_mask.shape) != expected_mm:
133
- raise ValueError(
134
- "multimodal mask/layout mismatch: "
135
- f"expected {expected_mm}, got {tuple(multimodal_attention_mask.shape)}"
136
- )
137
- if tuple(question_attention_mask.shape) != expected_q:
138
- raise ValueError(
139
- "question mask/layout mismatch: "
140
- f"expected {expected_q}, got {tuple(question_attention_mask.shape)}"
141
- )
142
- if layout.answer_input_length:
143
- if answer_input_attention_mask is None:
144
- raise ValueError("answer_input_attention_mask is required")
145
- expected_answer = (batch_size, layout.answer_input_length)
146
- if tuple(answer_input_attention_mask.shape) != expected_answer:
147
- raise ValueError(
148
- "answer mask/layout mismatch: "
149
- f"expected {expected_answer}, got "
150
- f"{tuple(answer_input_attention_mask.shape)}"
151
- )
152
- elif answer_input_attention_mask is not None and answer_input_attention_mask.numel():
153
- raise ValueError("received answer mask for an empty answer-input segment")
154
-
155
- device = multimodal_attention_mask.device
156
- length = layout.sequence_length
157
- visible = torch.zeros(
158
- batch_size, length, length, dtype=torch.bool, device=device
159
- )
160
- mm_valid = multimodal_attention_mask.bool()
161
- q_valid = question_attention_mask.bool()
162
- visual_valid = None
163
- if transition_conditioned or local_visual_chain:
164
- if not 1 <= question_visible_through_pair <= layout.num_pairs:
165
- raise ValueError(
166
- "question_visible_through_pair must lie in "
167
- f"[1,{layout.num_pairs}]"
168
- )
169
- if multimodal_visual_mask is None:
170
- raise ValueError(
171
- "multimodal_visual_mask is required for the "
172
- "transition-conditioned graph"
173
- )
174
- if tuple(multimodal_visual_mask.shape) != expected_mm:
175
- raise ValueError(
176
- "visual mask/layout mismatch: "
177
- f"expected {expected_mm}, got "
178
- f"{tuple(multimodal_visual_mask.shape)}"
179
- )
180
- visual_valid = multimodal_visual_mask.bool()
181
- if bool((visual_valid & ~mm_valid).any()):
182
- raise ValueError("visual keys must be a subset of valid multimodal keys")
183
-
184
- # The source multimodal prompt keeps the native causal graph. Padded query
185
- # rows are harmless and receive a self edge below.
186
- mm_causal = torch.ones(
187
- layout.multimodal_length,
188
- layout.multimodal_length,
189
- dtype=torch.bool,
190
- device=device,
191
- ).tril()
192
- visible[:, : layout.multimodal_length, : layout.multimodal_length] = (
193
- mm_causal.unsqueeze(0) & mm_valid[:, None, :]
194
- )
195
-
196
- # The duplicated question is deliberately text-only: it is causally
197
- # connected only to preceding valid rows of its own segment.
198
- q_causal = torch.ones(
199
- layout.question_length,
200
- layout.question_length,
201
- dtype=torch.bool,
202
- device=device,
203
- ).tril()
204
- visible[
205
- :, layout.question_slice, layout.question_slice
206
- ] = q_causal.unsqueeze(0) & q_valid[:, None, :]
207
-
208
- if local_visual_chain:
209
- # Homogeneous one-pass latent chain. Visual memory is persistent and
210
- # available at every position, while task state has exactly one local
211
- # predecessor edge. No latent may consume the complete causal prefix.
212
- latent_indices = range(layout.latent_start, layout.answer_start)
213
- for latent_offset, latent in enumerate(latent_indices):
214
- visible[:, latent, : layout.multimodal_length] = visual_valid
215
- if latent_offset == 0:
216
- visible[:, latent, layout.question_slice] = q_valid
217
- else:
218
- visible[:, latent, latent - 1] = True
219
-
220
- if layout.answer_input_length:
221
- answer_valid = answer_input_attention_mask.bool()
222
- answer_causal = torch.ones(
223
- layout.answer_input_length,
224
- layout.answer_input_length,
225
- dtype=torch.bool,
226
- device=device,
227
- ).tril()
228
- visible[:, layout.answer_slice, layout.question_slice] = q_valid[:, None, :]
229
- visible[:, layout.answer_slice, layout.answer_start - 1] = True
230
- visible[:, layout.answer_slice, layout.answer_slice] = (
231
- answer_causal.unsqueeze(0) & answer_valid[:, None, :]
232
- )
233
-
234
- diagonal = torch.arange(length, device=device)
235
- visible[:, diagonal, diagonal] = True
236
- return visible.unsqueeze(1)
237
-
238
- for pair_index, (look, think) in enumerate(
239
- zip(layout.look_indices, layout.think_indices)
240
- ):
241
- if final_pair_only and pair_index != layout.num_pairs - 1:
242
- continue
243
-
244
- if transition_conditioned and not final_pair_only:
245
- # Pair 1 bootstraps the causal latent prefix. Later pairs cannot
246
- # recover the task from the original prompt: only image placeholder
247
- # rows remain visible outside the latent prefix.
248
- if pair_index == 0:
249
- visible[:, look, : layout.multimodal_length] = mm_valid
250
- visible[:, look, layout.question_slice] = q_valid
251
- visible[:, think, layout.question_slice] = q_valid
252
- else:
253
- if not disable_late_visual:
254
- visible[:, look, : layout.multimodal_length] = visual_valid
255
- if not disable_chain_links:
256
- visible[:, look, layout.latent_start:look] = True
257
- visible[:, think, layout.latent_start:look] = True
258
- if pair_index < question_visible_through_pair:
259
- visible[:, look, layout.question_slice] = q_valid
260
- visible[:, think, layout.question_slice] = q_valid
261
-
262
- # The local LOOK -> THINK edge is never an inter-pair chain-link
263
- # intervention and therefore remains present in every arm.
264
- visible[:, think, look] = True
265
- else:
266
- # Original graph, also used by the final-pair-only capacity
267
- # control: each active pair may independently consume prompt + Q.
268
- if not disable_late_visual or pair_index == 0 or final_pair_only:
269
- visible[:, look, : layout.multimodal_length] = mm_valid
270
- visible[:, look, layout.question_slice] = q_valid
271
- if pair_index and not disable_chain_links and not final_pair_only:
272
- visible[:, look, layout.think_indices[pair_index - 1]] = True
273
-
274
- # Deliberation: no original multimodal key can be consumed here.
275
- visible[:, think, layout.question_slice] = q_valid
276
- visible[:, think, look] = True
277
-
278
- if layout.answer_input_length:
279
- answer_valid = answer_input_attention_mask.bool()
280
- answer_causal = torch.ones(
281
- layout.answer_input_length,
282
- layout.answer_input_length,
283
- dtype=torch.bool,
284
- device=device,
285
- ).tril()
286
- visible[:, layout.answer_slice, layout.question_slice] = q_valid[:, None, :]
287
- answer_thinks = (
288
- [layout.think_indices[-1]]
289
- if final_pair_only or transition_conditioned
290
- else list(layout.think_indices)
291
- )
292
- visible[:, layout.answer_slice, answer_thinks] = True
293
- visible[:, layout.answer_slice, layout.answer_slice] = (
294
- answer_causal.unsqueeze(0) & answer_valid[:, None, :]
295
- )
296
-
297
- # Avoid all-masked softmax rows for physical padding. Semantic queries do
298
- # not receive padded keys because every segment assignment above uses its
299
- # validity mask.
300
- diagonal = torch.arange(length, device=device)
301
- visible[:, diagonal, diagonal] = True
302
- return visible.unsqueeze(1)
303
-
304
-
305
- def build_perceive_deliberate_positions(
306
- layout: PerceiveDeliberateLayout,
307
- multimodal_position_ids: torch.LongTensor,
308
- multimodal_attention_mask: torch.Tensor,
309
- question_attention_mask: torch.Tensor,
310
- answer_input_attention_mask: torch.Tensor | None = None,
311
- ) -> torch.LongTensor:
312
- """Extend native multimodal M-RoPE with logical text positions.
313
-
314
- The clean question keeps ordinary relative text positions but is shifted
315
- after the largest valid multimodal M-RoPE coordinate. Latent and answer
316
- rows then continue from each item's *logical* question length, independent
317
- of right padding. All three M-RoPE coordinates are equal for these
318
- non-spatial rows.
319
- """
320
-
321
- if multimodal_position_ids.ndim != 3 or multimodal_position_ids.shape[0] != 3:
322
- raise ValueError("multimodal_position_ids must have shape [3, B, L_mm]")
323
- batch_size = multimodal_attention_mask.shape[0]
324
- if tuple(multimodal_position_ids.shape[1:]) != (
325
- batch_size,
326
- layout.multimodal_length,
327
- ):
328
- raise ValueError("multimodal position/layout mismatch")
329
-
330
- mm_valid = multimodal_attention_mask.bool()
331
- masked_mm = multimodal_position_ids.masked_fill(~mm_valid.unsqueeze(0), -1)
332
- continuation = masked_mm.amax(dim=(0, 2)).clamp_min(-1) + 1 # [B]
333
-
334
- q_valid = question_attention_mask.bool()
335
- q_relative = q_valid.long().cumsum(dim=-1) - 1
336
- q_relative = q_relative.masked_fill(~q_valid, 0)
337
- q_positions = continuation[:, None] + q_relative
338
- q_lengths = q_valid.sum(dim=-1)
339
-
340
- latent_relative = torch.arange(
341
- layout.num_latent_tokens,
342
- device=multimodal_position_ids.device,
343
- )
344
- latent_positions = (
345
- continuation[:, None] + q_lengths[:, None] + latent_relative[None, :]
346
- )
347
-
348
- segments = [multimodal_position_ids, q_positions.unsqueeze(0).expand(3, -1, -1)]
349
- segments.append(latent_positions.unsqueeze(0).expand(3, -1, -1))
350
- if layout.answer_input_length:
351
- if answer_input_attention_mask is None:
352
- raise ValueError("answer_input_attention_mask is required")
353
- answer_relative = torch.arange(
354
- layout.answer_input_length,
355
- device=multimodal_position_ids.device,
356
- )
357
- answer_positions = (
358
- continuation[:, None]
359
- + q_lengths[:, None]
360
- + layout.num_latent_tokens
361
- + answer_relative[None, :]
362
- )
363
- segments.append(answer_positions.unsqueeze(0).expand(3, -1, -1))
364
- return torch.cat(segments, dim=-1)
365
-
366
-
367
- def build_answer_decode_mask(
368
- layout: PerceiveDeliberateLayout,
369
- question_attention_mask: torch.Tensor,
370
- generated_attention_mask: torch.Tensor,
371
- *,
372
- local_visual_chain: bool = False,
373
- final_pair_only: bool = False,
374
- transition_conditioned: bool = False,
375
- ) -> torch.BoolTensor:
376
- """Visibility for one cached answer query after the latent prefill.
377
-
378
- ``generated_attention_mask`` includes the current answer token. Physical
379
- multimodal and LOOK cache rows remain present but are invisible.
380
- """
381
-
382
- if generated_attention_mask.ndim != 2:
383
- raise ValueError("generated_attention_mask must have shape [B, N]")
384
- batch_size, generated_length = generated_attention_mask.shape
385
- if tuple(question_attention_mask.shape) != (
386
- batch_size,
387
- layout.question_length,
388
- ):
389
- raise ValueError("question mask/layout mismatch")
390
- total_keys = layout.answer_start + generated_length
391
- visible = torch.zeros(
392
- batch_size, 1, 1, total_keys, dtype=torch.bool,
393
- device=question_attention_mask.device,
394
- )
395
- visible[:, 0, 0, layout.question_slice] = question_attention_mask.bool()
396
- if local_visual_chain:
397
- visible[:, 0, 0, layout.answer_start - 1] = True
398
- else:
399
- answer_thinks = (
400
- [layout.think_indices[-1]]
401
- if final_pair_only or transition_conditioned
402
- else list(layout.think_indices)
403
- )
404
- visible[:, 0, 0, answer_thinks] = True
405
- visible[:, 0, 0, layout.answer_start:] = generated_attention_mask.bool()
406
- return visible
407
-
408
-
409
- __all__ = [
410
- "PerceiveDeliberateLayout",
411
- "build_answer_decode_mask",
412
- "build_perceive_deliberate_mask",
413
- "build_perceive_deliberate_positions",
414
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
source_spatial.py DELETED
@@ -1,349 +0,0 @@
1
- """Question-conditioned recurrence over the complete native visual field.
2
-
3
- The state is the dynamic-resolution sequence emitted by Qwen2.5-VL's vision
4
- merger, before those image embeddings enter the language model. Tokens keep
5
- their native two-dimensional grid throughout the recurrence. One shared cell
6
- combines directional local messages with visual-to-question cross-attention::
7
-
8
- V_0 = VisionMerger(image)
9
- V_{k+1} = V_k + Cell(V_k, question, grid), k = 0, ..., T - 1
10
-
11
- Only ``V_T`` is inserted into the ordinary Qwen multimodal prefix. The cell's
12
- output projection is exactly zero at initialization, so every horizon,
13
- including the default T=8, is initially bitwise identical to the base visual
14
- embedding path. The full language model is then run from scratch; no cache
15
- created while constructing the visual field is available to answer decoding.
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- from dataclasses import dataclass
21
-
22
- import torch
23
- import torch.nn as nn
24
- import torch.nn.functional as F
25
-
26
- from transformers.models.qwen2.modeling_qwen2 import Qwen2RMSNorm
27
-
28
-
29
- @dataclass
30
- class SpatialVisualTelemetry:
31
- """Detached recurrence diagnostics; no visual trajectory is retained."""
32
-
33
- update_rms: torch.Tensor # [T]
34
- update_relative: torch.Tensor # [T]
35
- step_drift: torch.Tensor # [T], 1 - cos(V_k, V_{k+1})
36
- final_relative: torch.Tensor # scalar, RMS(V_T - V_0) / RMS(V_0)
37
-
38
-
39
- class SpatialVisualRecurrentCell(nn.Module):
40
- """Shared O(N) spatial cell for packed, variable-resolution image tokens.
41
-
42
- Global N-by-N visual attention is deliberately absent: a sample may carry
43
- 8192 merged visual tokens. Four directional grid messages preserve local
44
- geometry in linear time, while low-rank visual-to-question attention makes
45
- every patch update task dependent. The recurrent *state* remains at the
46
- native language-model width; ``inner_width`` limits only the update
47
- operator's compute and is not a state bottleneck.
48
- """
49
-
50
- def __init__(
51
- self,
52
- width: int,
53
- inner_width: int,
54
- num_heads: int,
55
- *,
56
- rms_norm_eps: float,
57
- residual_scale: float,
58
- ) -> None:
59
- super().__init__()
60
- if width < 1 or inner_width < 1:
61
- raise ValueError("width and inner_width must be positive")
62
- if num_heads < 1 or inner_width % num_heads:
63
- raise ValueError(
64
- "inner_width must be divisible by the positive num_heads"
65
- )
66
- if not 0.0 <= residual_scale <= 1.0:
67
- raise ValueError("residual_scale must lie in [0, 1]")
68
- self.width = int(width)
69
- self.inner_width = int(inner_width)
70
- self.num_heads = int(num_heads)
71
- self.head_dim = self.inner_width // self.num_heads
72
- self.residual_scale = float(residual_scale)
73
-
74
- self.state_norm = Qwen2RMSNorm(self.width, eps=rms_norm_eps)
75
- self.question_norm = Qwen2RMSNorm(self.width, eps=rms_norm_eps)
76
-
77
- # One projection contains distinct north/south/west/east maps. This
78
- # keeps orientation identifiable without allocating a dense 3x3 dxd
79
- # convolution over the 3584-wide native visual state.
80
- self.center_proj = nn.Linear(self.width, self.inner_width, bias=False)
81
- self.neighbor_proj = nn.Linear(
82
- self.width, 4 * self.inner_width, bias=False
83
- )
84
-
85
- self.query_proj = nn.Linear(self.width, self.inner_width, bias=False)
86
- self.key_proj = nn.Linear(self.width, self.inner_width, bias=False)
87
- self.value_proj = nn.Linear(self.width, self.inner_width, bias=False)
88
- self.output_proj = nn.Linear(self.inner_width, self.width, bias=False)
89
-
90
- def zero_output(self) -> None:
91
- """Restore the exact base-model identity after generic HF init."""
92
- with torch.no_grad():
93
- self.output_proj.weight.zero_()
94
-
95
- @staticmethod
96
- def _token_layout(
97
- merged_grid_thw: torch.LongTensor,
98
- image_to_batch: torch.LongTensor,
99
- batch_size: int,
100
- expected_tokens: int,
101
- ) -> tuple[torch.LongTensor, torch.LongTensor, torch.LongTensor]:
102
- """Return token-to-batch ids, within-sample positions and counts."""
103
- if merged_grid_thw.ndim != 2 or merged_grid_thw.shape[-1] != 3:
104
- raise ValueError("merged_grid_thw must have shape [num_images, 3]")
105
- if image_to_batch.ndim != 1 or image_to_batch.shape[0] != len(
106
- merged_grid_thw
107
- ):
108
- raise ValueError("image_to_batch must contain one id per image")
109
- per_image = merged_grid_thw.prod(dim=-1).long()
110
- if int(per_image.sum()) != int(expected_tokens):
111
- raise ValueError(
112
- "merged grids and visual field disagree: "
113
- f"grid tokens={int(per_image.sum())}, state tokens={expected_tokens}"
114
- )
115
- token_to_batch = torch.repeat_interleave(image_to_batch, per_image)
116
- if token_to_batch.numel() and (
117
- int(token_to_batch.min()) < 0
118
- or int(token_to_batch.max()) >= batch_size
119
- ):
120
- raise ValueError("image_to_batch contains an out-of-range batch id")
121
- if token_to_batch.numel() > 1 and bool(
122
- (token_to_batch[1:] < token_to_batch[:-1]).any()
123
- ):
124
- raise ValueError(
125
- "images must follow the same batch-major order as Qwen inputs"
126
- )
127
- counts = torch.bincount(token_to_batch, minlength=batch_size)
128
- starts = counts.cumsum(0) - counts
129
- within = torch.arange(
130
- expected_tokens, device=token_to_batch.device
131
- ) - torch.repeat_interleave(starts, counts)
132
- return token_to_batch, within, counts
133
-
134
- @staticmethod
135
- def _neighbor_edges(
136
- merged_grid_thw: torch.LongTensor,
137
- expected_tokens: int,
138
- ) -> tuple[torch.LongTensor, torch.LongTensor, torch.LongTensor]:
139
- """Build packed directed 4-neighbour edges once for all recurrent steps.
140
-
141
- Direction ids are 0/1/2/3 = north/south/west/east *as seen by the
142
- destination token*. Temporal slices are separate 2-D fields.
143
- """
144
- device = merged_grid_thw.device
145
- destinations: list[torch.Tensor] = []
146
- sources: list[torch.Tensor] = []
147
- directions: list[torch.Tensor] = []
148
- offset = 0
149
- for t_value, h_value, w_value in merged_grid_thw.detach().cpu().tolist():
150
- t, h, w = int(t_value), int(h_value), int(w_value)
151
- count = t * h * w
152
- if min(t, h, w) < 1:
153
- raise ValueError(f"invalid merged visual grid {(t, h, w)}")
154
- index = torch.arange(
155
- offset, offset + count, device=device, dtype=torch.long
156
- ).view(t, h, w)
157
-
158
- def append(dst: torch.Tensor, src: torch.Tensor, direction: int) -> None:
159
- if dst.numel() == 0:
160
- return
161
- destinations.append(dst.reshape(-1))
162
- sources.append(src.reshape(-1))
163
- directions.append(
164
- torch.full(
165
- (dst.numel(),), direction, device=device, dtype=torch.long
166
- )
167
- )
168
-
169
- append(index[:, 1:, :], index[:, :-1, :], 0) # north
170
- append(index[:, :-1, :], index[:, 1:, :], 1) # south
171
- append(index[:, :, 1:], index[:, :, :-1], 2) # west
172
- append(index[:, :, :-1], index[:, :, 1:], 3) # east
173
- offset += count
174
- if offset != expected_tokens:
175
- raise ValueError(
176
- f"edge grids contain {offset} tokens, expected {expected_tokens}"
177
- )
178
- if not destinations:
179
- empty = torch.empty(0, device=device, dtype=torch.long)
180
- return empty, empty, empty
181
- return (
182
- torch.cat(destinations),
183
- torch.cat(sources),
184
- torch.cat(directions),
185
- )
186
-
187
- def _question_attention(
188
- self,
189
- normalized_state: torch.Tensor,
190
- question_keys: torch.Tensor,
191
- question_values: torch.Tensor,
192
- question_valid: torch.BoolTensor,
193
- token_to_batch: torch.LongTensor,
194
- within_sample: torch.LongTensor,
195
- token_counts: torch.LongTensor,
196
- ) -> torch.Tensor:
197
- """Visual-query/text-memory SDPA for a packed visual sequence."""
198
- batch_size = question_keys.shape[0]
199
- max_tokens = int(token_counts.max()) if token_counts.numel() else 0
200
- if max_tokens == 0:
201
- return normalized_state.new_empty(0, self.inner_width)
202
-
203
- query_flat = self.query_proj(normalized_state)
204
- query = query_flat.new_zeros(
205
- batch_size, max_tokens, self.inner_width
206
- )
207
- query[token_to_batch, within_sample] = query_flat
208
-
209
- bsz, question_length, _ = question_keys.shape
210
- query = query.view(
211
- bsz, max_tokens, self.num_heads, self.head_dim
212
- ).transpose(1, 2)
213
- key = question_keys.view(
214
- bsz, question_length, self.num_heads, self.head_dim
215
- ).transpose(1, 2)
216
- value = question_values.view(
217
- bsz, question_length, self.num_heads, self.head_dim
218
- ).transpose(1, 2)
219
- # Boolean SDPA masks use True for entries that are allowed to attend.
220
- allowed = question_valid[:, None, None, :]
221
- attended = F.scaled_dot_product_attention(
222
- query,
223
- key,
224
- value,
225
- attn_mask=allowed,
226
- dropout_p=0.0,
227
- is_causal=False,
228
- )
229
- attended = attended.transpose(1, 2).reshape(
230
- bsz, max_tokens, self.inner_width
231
- )
232
- return attended[token_to_batch, within_sample]
233
-
234
- def forward(
235
- self,
236
- visual_state: torch.Tensor,
237
- merged_grid_thw: torch.LongTensor,
238
- image_to_batch: torch.LongTensor,
239
- question_embeddings: torch.Tensor,
240
- question_attention_mask: torch.Tensor | None,
241
- *,
242
- steps: int,
243
- ) -> tuple[torch.Tensor, SpatialVisualTelemetry]:
244
- if visual_state.ndim != 2 or visual_state.shape[-1] != self.width:
245
- raise ValueError(
246
- f"visual_state must be [N,{self.width}], got "
247
- f"{tuple(visual_state.shape)}"
248
- )
249
- if question_embeddings.ndim != 3 or question_embeddings.shape[-1] != self.width:
250
- raise ValueError(
251
- f"question_embeddings must be [B,Q,{self.width}]"
252
- )
253
- if steps < 0:
254
- raise ValueError("steps must be non-negative")
255
- batch_size = question_embeddings.shape[0]
256
- token_to_batch, within_sample, token_counts = self._token_layout(
257
- merged_grid_thw,
258
- image_to_batch,
259
- batch_size,
260
- visual_state.shape[0],
261
- )
262
- edge_dst, edge_src, edge_direction = self._neighbor_edges(
263
- merged_grid_thw, visual_state.shape[0]
264
- )
265
-
266
- if question_attention_mask is None:
267
- question_valid = torch.ones(
268
- question_embeddings.shape[:2],
269
- dtype=torch.bool,
270
- device=question_embeddings.device,
271
- )
272
- else:
273
- if question_attention_mask.shape != question_embeddings.shape[:2]:
274
- raise ValueError("question attention mask shape mismatch")
275
- question_valid = question_attention_mask > 0
276
- if not bool(question_valid.any(dim=-1).all()):
277
- raise ValueError("every image-bearing sample needs a question token")
278
-
279
- normalized_question = self.question_norm(question_embeddings)
280
- question_keys = self.key_proj(normalized_question)
281
- question_values = self.value_proj(normalized_question)
282
-
283
- initial = visual_state
284
- state = visual_state
285
- update_rms: list[torch.Tensor] = []
286
- update_relative: list[torch.Tensor] = []
287
- step_drift: list[torch.Tensor] = []
288
- for _ in range(steps):
289
- normalized = self.state_norm(state)
290
- center = self.center_proj(normalized)
291
- directional = self.neighbor_proj(normalized).view(
292
- state.shape[0], 4, self.inner_width
293
- )
294
- spatial = center.new_zeros(center.shape)
295
- degree = center.new_zeros(center.shape[0], 1)
296
- if edge_dst.numel():
297
- messages = directional[edge_src, edge_direction]
298
- spatial.index_add_(0, edge_dst, messages)
299
- degree.index_add_(
300
- 0,
301
- edge_dst,
302
- torch.ones(
303
- edge_dst.shape[0], 1, dtype=center.dtype, device=center.device
304
- ),
305
- )
306
- spatial = spatial / degree.clamp_min(1.0)
307
- question = self._question_attention(
308
- normalized,
309
- question_keys,
310
- question_values,
311
- question_valid,
312
- token_to_batch,
313
- within_sample,
314
- token_counts,
315
- )
316
- mixed = F.silu((center + spatial + question) / (3.0**0.5))
317
- update = self.output_proj(mixed) * self.residual_scale
318
- next_state = state + update
319
-
320
- with torch.no_grad():
321
- state_rms = state.float().square().mean().sqrt().clamp_min(1e-8)
322
- update_norm = update.float().square().mean().sqrt()
323
- cosine = F.cosine_similarity(
324
- state.float(), next_state.float(), dim=-1
325
- ).mean()
326
- update_rms.append(update_norm.detach())
327
- update_relative.append((update_norm / state_rms).detach())
328
- step_drift.append((1.0 - cosine).detach())
329
- state = next_state
330
-
331
- with torch.no_grad():
332
- initial_rms = initial.float().square().mean().sqrt().clamp_min(1e-8)
333
- final_relative = (
334
- (state.float() - initial.float()).square().mean().sqrt()
335
- / initial_rms
336
- ).detach()
337
- empty = initial.new_empty(0, dtype=torch.float32)
338
- telemetry = SpatialVisualTelemetry(
339
- update_rms=(torch.stack(update_rms) if update_rms else empty),
340
- update_relative=(
341
- torch.stack(update_relative) if update_relative else empty
342
- ),
343
- step_drift=(torch.stack(step_drift) if step_drift else empty),
344
- final_relative=final_relative,
345
- )
346
- return state, telemetry
347
-
348
-
349
- __all__ = ["SpatialVisualRecurrentCell", "SpatialVisualTelemetry"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
source_splitting.py DELETED
@@ -1,473 +0,0 @@
1
- """Layer-split forward for Qwen2.5-VL and Qwen3-VL.
2
-
3
- Method §3.2 needs ``F = F_{>l*} o F_{<=l*}`` as two separately runnable halves so
4
- that the multimodal branch can be cut off at ``l*`` and replaced by the
5
- workspace. §3.1 needs the same split to read and patch activations at a chosen
6
- depth.
7
-
8
- This module reimplements the prologue of the native Qwen VL text-model forward
9
- -- embedding merge, M-RoPE index, causal mask, rotary embeddings -- as a
10
- reusable :class:`SplitContext`, then exposes the decoder-layer loop as a range
11
- you can run piecewise. Qwen3-VL additionally injects three ``DeepStack``
12
- vision features after language layers 0--2; those tensors are carried in the
13
- split context and applied at the identical layer boundaries. It deliberately
14
- mirrors transformers 4.57.6 rather than monkeypatching it;
15
- ``tests/test_split_equivalence.py`` asserts the composed halves reproduce the
16
- stock forward bit-for-bit, which is what makes the mirroring safe to rely on.
17
-
18
- Shapes use ``B`` batch, ``L`` sequence, ``d`` backbone width (3584 on the 7B),
19
- ``N_v`` visual tokens, ``N_q`` question tokens.
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- from dataclasses import dataclass, replace
25
- from typing import Any
26
-
27
- import torch
28
- from transformers.cache_utils import Cache
29
- from transformers.masking_utils import (
30
- create_causal_mask,
31
- create_sliding_window_causal_mask,
32
- )
33
-
34
-
35
- @dataclass
36
- class SplitContext:
37
- """Per-forward state shared by every decoder layer.
38
-
39
- Computed once by :func:`make_split_context` so that layer ranges can be run
40
- independently without recomputing masks or rotary tables.
41
- """
42
-
43
- hidden_states: torch.Tensor # [B, L, d] - mutated as layers run
44
- position_ids: torch.Tensor # [3, B, L] - M-RoPE (t, h, w)
45
- position_embeddings: tuple[torch.Tensor, torch.Tensor] # (cos, sin) [B, L, head_dim]
46
- causal_mask_mapping: dict[str, torch.Tensor | None]
47
- cache_position: torch.Tensor # [L]
48
- text_position_ids: torch.Tensor | None # [B, L] only when packed
49
- past_key_values: Cache | None
50
- # Original 2-D key-padding mask. ``create_causal_mask`` is allowed to
51
- # return ``None`` for SDPA and delegate causality to ``is_causal``; keeping
52
- # this tensor lets counterfactual branches materialize the equivalent mask
53
- # before removing a precisely selected set of attention edges.
54
- attention_mask: torch.Tensor | None = None # [B, L_kv]
55
- # Qwen3-VL only. DeepStack adds one visual feature tensor after each of
56
- # the first three language layers. They stay ``None`` for Qwen2.5-VL and
57
- # for every text-only branch.
58
- visual_pos_masks: torch.Tensor | None = None # [B, L] bool
59
- deepstack_visual_embeds: list[torch.Tensor] | None = None
60
-
61
- def clone_at(self, hidden_states: torch.Tensor) -> "SplitContext":
62
- """Same context, different hidden states (for patched re-runs)."""
63
- return SplitContext(
64
- hidden_states=hidden_states,
65
- position_ids=self.position_ids,
66
- position_embeddings=self.position_embeddings,
67
- causal_mask_mapping=self.causal_mask_mapping,
68
- cache_position=self.cache_position,
69
- text_position_ids=self.text_position_ids,
70
- past_key_values=self.past_key_values,
71
- attention_mask=self.attention_mask,
72
- visual_pos_masks=self.visual_pos_masks,
73
- deepstack_visual_embeds=self.deepstack_visual_embeds,
74
- )
75
-
76
-
77
- # ---------------------------------------------------------------------------
78
- # embedding / position construction
79
- # ---------------------------------------------------------------------------
80
-
81
-
82
- def embed_multimodal(
83
- vl_model,
84
- input_ids: torch.LongTensor, # [B, L]
85
- pixel_values: torch.Tensor | None = None,
86
- image_grid_thw: torch.LongTensor | None = None,
87
- attention_mask: torch.Tensor | None = None,
88
- *,
89
- return_deepstack: bool = False,
90
- ) -> (
91
- tuple[torch.Tensor, torch.Tensor]
92
- | tuple[
93
- torch.Tensor,
94
- torch.Tensor,
95
- torch.Tensor | None,
96
- list[torch.Tensor] | None,
97
- ]
98
- ):
99
- """Token embeddings with image features scattered in, plus M-RoPE indices.
100
-
101
- Mirrors the prefill path of ``Qwen2_5_VLModel.forward``. Pass
102
- ``pixel_values=None`` to get the text-only branch used for ``Q*``.
103
-
104
- Args:
105
- vl_model: a native ``Qwen2_5_VLModel`` or ``Qwen3VLModel`` (i.e.
106
- ``model.model``, not the ``...ForConditionalGeneration`` wrapper).
107
- return_deepstack: also return Qwen3-VL's visual-position mask and
108
- DeepStack features. The default two-value return keeps all
109
- Qwen2.5 callers backward compatible.
110
-
111
- Returns:
112
- ``(inputs_embeds [B, L, d], position_ids [3, B, L])`` and optionally
113
- ``(visual_pos_masks, deepstack_visual_embeds)``.
114
- """
115
- inputs_embeds = vl_model.get_input_embeddings()(input_ids) # [B, L, d]
116
- model_type = str(getattr(vl_model.config, "model_type", ""))
117
- # A freshly wrapped model exposes the native qwen3_vl config here.
118
- # Reloading a fully saved CLOSE checkpoint reconstructs the nested native
119
- # backbone from the wrapper config, so its model view carries
120
- # close_qwen3_vl instead. Both use the tuple-returning Qwen3 feature API.
121
- is_qwen3_vl = model_type in {"qwen3_vl", "close_qwen3_vl"}
122
- visual_pos_masks = None
123
- deepstack_visual_embeds = None
124
-
125
- if pixel_values is not None:
126
- image_features = vl_model.get_image_features(pixel_values, image_grid_thw)
127
- if is_qwen3_vl:
128
- image_embeds, deepstack_visual_embeds = image_features
129
- else:
130
- image_embeds = image_features
131
- image_embeds = torch.cat(image_embeds, dim=0).to(
132
- inputs_embeds.device, inputs_embeds.dtype
133
- ) # [N_v_total, d]
134
- image_mask, _ = vl_model.get_placeholder_mask(
135
- input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds
136
- )
137
- inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
138
- if is_qwen3_vl:
139
- visual_pos_masks = image_mask[..., 0]
140
-
141
- if is_qwen3_vl:
142
- position_ids, _ = vl_model.get_rope_index(
143
- input_ids,
144
- image_grid_thw,
145
- None, # video_grid_thw
146
- attention_mask=attention_mask,
147
- )
148
- else:
149
- position_ids, _ = vl_model.get_rope_index(
150
- input_ids,
151
- image_grid_thw,
152
- None, # video_grid_thw
153
- second_per_grid_ts=None,
154
- attention_mask=attention_mask,
155
- )
156
- if return_deepstack:
157
- return (
158
- inputs_embeds,
159
- position_ids,
160
- visual_pos_masks,
161
- deepstack_visual_embeds,
162
- )
163
- return inputs_embeds, position_ids
164
-
165
-
166
- def make_split_context(
167
- text_model,
168
- inputs_embeds: torch.Tensor, # [B, L, d]
169
- position_ids: torch.Tensor, # [3, B, L]
170
- attention_mask: torch.Tensor | None = None,
171
- past_key_values: Cache | None = None,
172
- cache_position: torch.Tensor | None = None,
173
- visual_pos_masks: torch.Tensor | None = None,
174
- deepstack_visual_embeds: list[torch.Tensor] | None = None,
175
- ) -> SplitContext:
176
- """Build masks and rotary embeddings once, as the stock forward does.
177
-
178
- Args:
179
- text_model: ``Qwen2_5_VLTextModel`` (``vl_model.language_model``).
180
- """
181
- if cache_position is None:
182
- past_seen = past_key_values.get_seq_length() if past_key_values is not None else 0
183
- cache_position = torch.arange(
184
- past_seen, past_seen + inputs_embeds.shape[1], device=inputs_embeds.device
185
- ) # [L]
186
-
187
- if position_ids.ndim == 2:
188
- position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
189
-
190
- model_type = str(getattr(text_model.config, "model_type", ""))
191
-
192
- # Packed-sequence convention: a leading text-only row makes it [4, B, L].
193
- if position_ids.ndim == 3 and position_ids.shape[0] == 4:
194
- text_position_ids = position_ids[0] # [B, L]
195
- position_ids = position_ids[1:] # [3, B, L]
196
- elif model_type == "qwen3_vl_text":
197
- # Qwen3-VL always passes the temporal M-RoPE row to both the causal-mask
198
- # builder and decoder layers, even for ordinary (non-packed) inputs.
199
- text_position_ids = position_ids[0]
200
- else:
201
- text_position_ids = None
202
-
203
- mask_kwargs: dict[str, Any] = {
204
- "config": text_model.config,
205
- "input_embeds": inputs_embeds,
206
- "attention_mask": attention_mask,
207
- "cache_position": cache_position,
208
- "past_key_values": past_key_values,
209
- "position_ids": text_position_ids,
210
- }
211
- causal_mask_mapping = {"full_attention": create_causal_mask(**mask_kwargs)}
212
- if getattr(text_model, "has_sliding_layers", False):
213
- causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(
214
- **mask_kwargs
215
- )
216
-
217
- position_embeddings = text_model.rotary_emb(inputs_embeds, position_ids)
218
-
219
- return SplitContext(
220
- hidden_states=inputs_embeds,
221
- position_ids=position_ids,
222
- position_embeddings=position_embeddings,
223
- causal_mask_mapping=causal_mask_mapping,
224
- cache_position=cache_position,
225
- text_position_ids=text_position_ids,
226
- past_key_values=past_key_values,
227
- attention_mask=attention_mask,
228
- visual_pos_masks=visual_pos_masks,
229
- deepstack_visual_embeds=deepstack_visual_embeds,
230
- )
231
-
232
-
233
- def block_attention_edges(
234
- ctx: SplitContext,
235
- query_mask: torch.Tensor,
236
- key_mask: torch.Tensor,
237
- ) -> SplitContext:
238
- """Return ``ctx`` with selected query-to-key attention edges removed.
239
-
240
- ``query_mask`` and ``key_mask`` are boolean ``[B, L]`` supports in the
241
- current (cache-free) sequence. Every ordinary causal/padding constraint is
242
- preserved; only their Cartesian product is additionally masked. Both
243
- boolean SDPA masks (``True`` means visible) and additive eager masks
244
- (``0``/negative infinity) are supported.
245
-
246
- The helper deliberately rejects cached contexts. Its intended use is a
247
- counterfactual recurrent layer evaluation, never autoregressive decoding,
248
- and silently guessing the key offset of a populated cache would invalidate
249
- the causal comparison.
250
- """
251
- if ctx.past_key_values is not None and ctx.past_key_values.get_seq_length() > 0:
252
- raise ValueError("block_attention_edges requires a cache-free context")
253
- if query_mask.dtype != torch.bool or key_mask.dtype != torch.bool:
254
- raise TypeError("query_mask and key_mask must be boolean tensors")
255
- if query_mask.shape != key_mask.shape or query_mask.ndim != 2:
256
- raise ValueError(
257
- "query_mask and key_mask must have the same [B, L] shape, got "
258
- f"{tuple(query_mask.shape)} and {tuple(key_mask.shape)}"
259
- )
260
- batch_size, seq_len = query_mask.shape
261
- if ctx.hidden_states.shape[:2] != (batch_size, seq_len):
262
- raise ValueError(
263
- "edge masks must match the SplitContext sequence, got "
264
- f"{tuple(query_mask.shape)} for {tuple(ctx.hidden_states.shape[:2])}"
265
- )
266
-
267
- blocked = query_mask[:, None, :, None] & key_mask[:, None, None, :]
268
- updated: dict[str, torch.Tensor] = {}
269
- for attention_type, base_mask in ctx.causal_mask_mapping.items():
270
- if base_mask is None:
271
- # SDPA may omit an all-valid causal mask. Materialize exactly that
272
- # lower triangle, then reapply key padding before deleting edges.
273
- q_positions = ctx.cache_position
274
- if q_positions.numel() != seq_len:
275
- raise ValueError(
276
- "cache-free context must have one cache position per row"
277
- )
278
- key_positions = torch.arange(seq_len, device=query_mask.device)
279
- visible = key_positions[None, :] <= q_positions[:, None]
280
- visible = visible[None, None].expand(batch_size, 1, -1, -1)
281
- if ctx.attention_mask is not None:
282
- if ctx.attention_mask.shape != (batch_size, seq_len):
283
- raise ValueError(
284
- "counterfactual edge masking expects a 2-D [B, L] "
285
- "attention mask"
286
- )
287
- visible = visible & ctx.attention_mask[:, None, None, :].bool()
288
- updated[attention_type] = visible & ~blocked
289
- continue
290
-
291
- if not isinstance(base_mask, torch.Tensor) or base_mask.ndim != 4:
292
- raise TypeError(
293
- "counterfactual edge masking supports tensor 4-D attention "
294
- f"masks, got {type(base_mask)!r}"
295
- )
296
- if base_mask.shape[0] not in (1, batch_size):
297
- raise ValueError("attention-mask batch dimension is incompatible")
298
- if base_mask.shape[-2:] != (seq_len, seq_len):
299
- raise ValueError(
300
- "counterfactual edge masking expects a square cache-free mask, "
301
- f"got {tuple(base_mask.shape)}"
302
- )
303
- if base_mask.dtype == torch.bool:
304
- updated[attention_type] = base_mask & ~blocked
305
- elif base_mask.is_floating_point():
306
- updated[attention_type] = base_mask.masked_fill(
307
- blocked, torch.finfo(base_mask.dtype).min
308
- )
309
- else:
310
- raise TypeError(
311
- f"unsupported attention mask dtype {base_mask.dtype}"
312
- )
313
-
314
- return replace(ctx, causal_mask_mapping=updated)
315
-
316
-
317
- # ---------------------------------------------------------------------------
318
- # running layer ranges
319
- # ---------------------------------------------------------------------------
320
-
321
-
322
- def run_layer_range(
323
- text_model,
324
- ctx: SplitContext,
325
- start: int,
326
- stop: int | None = None,
327
- use_cache: bool = False,
328
- hidden_states: torch.Tensor | None = None,
329
- collect: bool = False,
330
- ) -> torch.Tensor | tuple[torch.Tensor, list[torch.Tensor]]:
331
- """Run ``text_model.layers[start:stop]`` on ``ctx``.
332
-
333
- ``self.norm`` is *not* applied -- it belongs to the very top of the stack.
334
- Call :func:`final_norm` after the last range.
335
-
336
- Args:
337
- hidden_states: override the context's states (leave ``None`` to chain).
338
- collect: also return the input hidden states of every layer in the range
339
- plus the range output, i.e. ``stop - start + 1`` tensors.
340
-
341
- Returns:
342
- ``[B, L, d]``, or ``(output, collected)`` when ``collect``.
343
- """
344
- layers = text_model.layers
345
- stop = len(layers) if stop is None else stop
346
- h = ctx.hidden_states if hidden_states is None else hidden_states
347
-
348
- collected: list[torch.Tensor] = []
349
- for layer_index, layer in enumerate(layers[start:stop], start=start):
350
- if collect:
351
- collected.append(h)
352
- attention_type = getattr(layer, "attention_type", "full_attention")
353
- h = layer(
354
- h,
355
- attention_mask=ctx.causal_mask_mapping[attention_type],
356
- position_ids=ctx.text_position_ids,
357
- past_key_values=ctx.past_key_values,
358
- use_cache=use_cache,
359
- cache_position=ctx.cache_position,
360
- position_embeddings=ctx.position_embeddings,
361
- )
362
- # 4.57 decoder layers return a bare tensor; older ones returned a tuple.
363
- if isinstance(h, tuple):
364
- h = h[0]
365
- if (
366
- ctx.deepstack_visual_embeds is not None
367
- and layer_index < len(ctx.deepstack_visual_embeds)
368
- ):
369
- if ctx.visual_pos_masks is None:
370
- raise ValueError("DeepStack features require visual_pos_masks")
371
- h = text_model._deepstack_process(
372
- h,
373
- ctx.visual_pos_masks,
374
- ctx.deepstack_visual_embeds[layer_index],
375
- )
376
-
377
- if collect:
378
- collected.append(h)
379
- return h, collected
380
- return h
381
-
382
-
383
- def final_norm(text_model, hidden_states: torch.Tensor) -> torch.Tensor:
384
- """Apply the stack's final RMSNorm. ``[B, L, d] -> [B, L, d]``."""
385
- return text_model.norm(hidden_states)
386
-
387
-
388
- # ---------------------------------------------------------------------------
389
- # token selection operators (Pi_img / Pi_q in §3.2)
390
- # ---------------------------------------------------------------------------
391
-
392
-
393
- def image_token_mask(input_ids: torch.LongTensor, image_token_id: int) -> torch.Tensor:
394
- """``Pi_img`` support: ``[B, L]`` bool, True at image placeholder positions."""
395
- return input_ids == image_token_id
396
-
397
-
398
- def vision_span_mask(input_ids: torch.LongTensor, config) -> torch.Tensor:
399
- """``[B, L]`` bool covering ``<|vision_start|>``, image pads, ``<|vision_end|>``.
400
-
401
- Use this (not :func:`image_token_mask`) when *removing* the visual segment to
402
- build the text-only branch, so the delimiters do not survive as orphans.
403
- """
404
- ids = {
405
- config.vision_start_token_id,
406
- config.vision_end_token_id,
407
- config.image_token_id,
408
- config.video_token_id,
409
- }
410
- mask = torch.zeros_like(input_ids, dtype=torch.bool)
411
- for tid in ids:
412
- mask |= input_ids == tid
413
- return mask
414
-
415
-
416
- def select_tokens(
417
- hidden_states: torch.Tensor, # [B, L, d]
418
- mask: torch.Tensor, # [B, L] bool
419
- ) -> torch.Tensor:
420
- """Gather masked positions. Requires an equal count per batch element.
421
-
422
- Returns ``[B, N, d]`` where ``N`` is that per-element count.
423
- """
424
- counts = mask.sum(dim=1)
425
- if counts.numel() > 1 and not bool((counts == counts[0]).all()):
426
- raise ValueError(
427
- f"select_tokens needs the same number of selected tokens per batch "
428
- f"element, got {counts.tolist()}. Bucket by visual-token count or "
429
- f"gather per-example instead."
430
- )
431
- n = int(counts[0])
432
- b, _, d = hidden_states.shape
433
- return hidden_states[mask].view(b, n, d)
434
-
435
-
436
- def select_tokens_padded(
437
- hidden_states: torch.Tensor, # [B, L, d]
438
- mask: torch.Tensor, # [B, L] bool
439
- ) -> tuple[torch.Tensor, torch.Tensor]:
440
- """Gather masked positions, right-padded to the batch maximum.
441
-
442
- Qwen2.5-VL uses dynamic resolution, so ``N_v`` differs across a batch. §3.1's
443
- patching genuinely needs equal counts (it transplants position by position),
444
- but ``r_theta`` only cross-attends over ``V*`` -- a variable-length memory is
445
- exactly what a key-padding mask is for.
446
-
447
- Returns ``(padded [B, N_max, d], key_padding_mask [B, N_max])`` where the
448
- mask is ``True`` at padding, matching ``nn.MultiheadAttention``.
449
- """
450
- counts = mask.sum(dim=1)
451
- n_max = int(counts.max())
452
- b, _, d = hidden_states.shape
453
- out = hidden_states.new_zeros((b, n_max, d))
454
- pad = torch.ones((b, n_max), dtype=torch.bool, device=hidden_states.device)
455
- for i in range(b):
456
- n = int(counts[i])
457
- out[i, :n] = hidden_states[i][mask[i]]
458
- pad[i, :n] = False
459
- return out, pad
460
-
461
-
462
- __all__ = [
463
- "SplitContext",
464
- "embed_multimodal",
465
- "make_split_context",
466
- "block_attention_edges",
467
- "run_layer_range",
468
- "final_norm",
469
- "image_token_mask",
470
- "vision_span_mask",
471
- "select_tokens",
472
- "select_tokens_padded",
473
- ]