File size: 17,198 Bytes
2dc3625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""

SCoPE: Sightline-Coordinate Positional Encoding โ€” Normalize-Gate-Inject



็”จไบŽ่งฃๅ†ณ่ทจๆ•ฐๆฎ้›† scale ไธไธ€่‡ดๅฏผ่‡ด็š„ๆ•ฐๅ€ผไธ็จณๅฎš้—ฎ้ข˜ใ€‚



ๆ ธๅฟƒๆœบๅˆถ๏ผš

  1. ๅฐ† 6D Plรผcker ๅๆ ‡ (d, m) ่งฃ่€ฆไธบๅฝ’ไธ€ๅŒ–ๅ‡ ไฝ• (d, mฬ‚) + ๅฏนๆ•ฐๅฐบๅบฆ logโ€–mโ€–

     โ†’ ๆŠ•ๅฝฑ่พ“ๅ…ฅๅ˜ไธบ 7D๏ผŒๅ‡ ไฝ•ๆ–นๅ‘ scale-invariant

  2. ๅขžๅŠ  scale_gate: log_scale โ†’ sigmoid(MLP) โˆˆ (0,1)

     โ†’ ๅŠจๆ€่ฐƒ่Š‚ PE ๆณจๅ…ฅๅผบๅบฆ๏ผš่ฟ‘ๆ™ฏๅผบใ€่ฟœๆ™ฏๅผฑ

  3. log_scale ๅŒๆ—ถๅ‚ไธŽ E_q / E_k ๆŠ•ๅฝฑ๏ผŒไธไธขๅคฑ็ปๅฏน่ท็ฆปไฟกๆฏ

  4. PE ่พ“ๅ‡บ็ป RMSNorm ๅฝ’ไธ€ๅŒ–๏ผŒไธŽ content path ็š„ QKNorm ๅฏน็งฐ

     โ†’ ฮฑ ็›ดๆŽฅๆŽงๅˆถ geometry/content ็š„็›ธๅฏนๆฏ”ไพ‹



ๆ•ฐๅญฆๅฝขๅผ๏ผš

    d_i, mฬ‚_i, s_i = decompose(r_i)           # d ไธๅ˜, mฬ‚=m/โ€–mโ€–, s=logโ€–mโ€–

    pe_q_i = gate(s_i) ยท ฮฑ_q ยท RMSNorm(E_q(d_i, mฬ‚_i, s_i))

    pe_k_j = gate(s_j) ยท ฮฑ_k ยท RMSNorm(E_k(mฬ‚_j, d_j, s_j))   โ† flip (d,mฬ‚)

    q_i = QKNorm(W_Q x_i) + pe_q_i

    k_j = QKNorm(W_K x_j) + pe_k_j



ฮฑ=1.0 ๆ—ถ geometry ไธŽ content ็ญ‰ๆƒๅ‚ไธŽ attentionใ€‚



Usage:

    pe = SightlineCoordinatePE(dim=1536, plucker_init="zero", plucker_scale=1.0)

    q, k = pe.apply_to_qk(q, k, plucker_6d)

    # or with cam_residual:

    q, k, cam_res = pe.apply_to_qk_and_output(q, k, plucker_6d, num_frames=21)

"""

import torch
from torch import nn


class _RMSNorm(nn.Module):
    """Per-token RMSNorm with learnable scale (matches WAN's QKNorm)."""

    def __init__(self, dim: int, eps: float = 1e-6):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps) * self.weight


class SightlineCoordinatePE(nn.Module):
    """

    Sightline-coordinate positional encoding with Normalize-Gate-Inject.



    Args:

        dim: attention feature dimension (e.g. 3072 for 5B).

        plucker_init: "zero" or "small" for E_q/E_k initialization.

        plucker_init_scale: std for "small" init.

        plucker_mlp_hidden: if > 0, use 7โ†’hiddenโ†’dim MLP; if 0, use 7โ†’dim Linear.

        plucker_scale: if > 0, add learnable ฮฑ_q/ฮฑ_k initialized to this value.

            With PE RMSNorm, ฮฑ=1.0 means geometry and content contribute equally.

        gate_init_bias: initial value for cam_residual gate logit (only used

            when enable_cam_residual=True).

        enable_cam_residual: whether to add frame-uniform gated camera residual.

        scale_gate_hidden: hidden dim of the scale gate MLP. Defaults to dim // 4.

        log_scale_aug_prob: probability of applying a uniform per-sample shift

            to the log_scale that feeds the scale_gate MLP during training.

            0.0 = disabled (backward compatible).  Only `scale_gate` input is

            perturbed; feat_q/feat_k (E_q/E_k inputs) keep the true log_scale.

            Only active when self.training is True.

        log_scale_aug_range: (lo, hi) tuple of the uniform shift range in

            natural-log units.  Default (-1.2, 1.6) spans roughly รท3.3 โ€ฆ ร—5.

    """

    def __init__(

        self,

        dim: int,

        plucker_init: str = "zero",

        plucker_init_scale: float = 0.01,

        plucker_mlp_hidden: int = 0,

        plucker_scale: float = 0.0,

        gate_init_bias: float = -2.0,

        enable_cam_residual: bool = True,

        scale_gate_hidden: int = 0,

        log_scale_aug_prob: float = 0.0,

        log_scale_aug_range: tuple = (-1.2, 1.6),

    ):
        super().__init__()
        self.dim = dim
        self.use_mlp = plucker_mlp_hidden > 0
        self.use_scale = plucker_scale > 0
        self.enable_cam_residual = enable_cam_residual
        self.log_scale_aug_prob = float(log_scale_aug_prob)
        self.log_scale_aug_range = (float(log_scale_aug_range[0]), float(log_scale_aug_range[1]))

        in_dim = 7  # (d(3), mฬ‚(3), log_s(1))

        # โ”€โ”€ Q/K geometric projections โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        if self.use_mlp:
            self.eq = nn.Sequential(
                nn.Linear(in_dim, plucker_mlp_hidden, bias=False),
                nn.GELU(),
                nn.Linear(plucker_mlp_hidden, dim, bias=False),
            )
            self.ek = nn.Sequential(
                nn.Linear(in_dim, plucker_mlp_hidden, bias=False),
                nn.GELU(),
                nn.Linear(plucker_mlp_hidden, dim, bias=False),
            )
        else:
            self.eq = nn.Linear(in_dim, dim, bias=False)
            self.ek = nn.Linear(in_dim, dim, bias=False)

        # โ”€โ”€ PE RMSNorm: align PE magnitude with content QKNorm โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        self.norm_pe_q = _RMSNorm(dim)
        self.norm_pe_k = _RMSNorm(dim)

        # โ”€โ”€ Scale gate: log_scale โ†’ (0, 1) per-dim โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        sg_hidden = scale_gate_hidden if scale_gate_hidden > 0 else max(dim // 4, 1)
        self.scale_gate = nn.Sequential(
            nn.Linear(1, sg_hidden),
            nn.SiLU(),
            nn.Linear(sg_hidden, dim),
            nn.Sigmoid(),
        )
        # init gate bias so output โ‰ˆ 0.5 at start (log_scale=0 โ†’ neutral)
        nn.init.zeros_(self.scale_gate[0].bias)
        nn.init.zeros_(self.scale_gate[2].bias)

        # โ”€โ”€ Learnable per-layer scale ฮฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        # Shape (1,) instead of () because FSDP refuses to shard 0-dim
        # parameters. Broadcasting `alpha * pe_q` is identical for both shapes.
        if self.use_scale:
            self.alpha_q = nn.Parameter(torch.tensor([plucker_scale]))
            self.alpha_k = nn.Parameter(torch.tensor([plucker_scale]))

        # Optional camera residual.โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        if self.enable_cam_residual:
            if self.use_mlp:
                self.ev = nn.Sequential(
                    nn.Linear(in_dim, plucker_mlp_hidden, bias=False),
                    nn.GELU(),
                    nn.Linear(plucker_mlp_hidden, dim, bias=False),
                )
                self.gate_proj = nn.Sequential(
                    nn.Linear(in_dim, plucker_mlp_hidden, bias=True),
                    nn.GELU(),
                    nn.Linear(plucker_mlp_hidden, dim, bias=False),
                )
            else:
                self.ev = nn.Linear(in_dim, dim, bias=False)
                self.gate_proj = nn.Linear(in_dim, dim, bias=False)
            self.gate_logit = nn.Parameter(torch.full((dim,), gate_init_bias))

        self._init_weights(plucker_init, plucker_init_scale)

    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    # Backward-compat ckpt loading
    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    def _load_from_state_dict(

        self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs

    ):
        # alpha_q / alpha_k were 0-dim scalars in earlier checkpoints; FSDP
        # requires shape (1,). Promote legacy entries while loading.
        for name in ("alpha_q", "alpha_k"):
            key = prefix + name
            if key in state_dict and state_dict[key].dim() == 0:
                state_dict[key] = state_dict[key].view(1)
        super()._load_from_state_dict(
            state_dict,
            prefix,
            local_metadata,
            strict,
            missing_keys,
            unexpected_keys,
            error_msgs,
        )

    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    # Weight init
    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    def _init_weights(self, mode: str, scale: float):
        if self.use_mlp:
            # For 2-layer MLP with zero init: only zero the OUTPUT layer.
            # Zeroing both layers creates dead gradients (h=GELU(0)=0 โ†’ โˆ‚L/โˆ‚W=0).
            # The first layer keeps default kaiming init so hidden activations โ‰  0.
            qk_output_layers = [self.eq[2], self.ek[2]]
            qk_input_layers = [self.eq[0], self.ek[0]]
        else:
            qk_output_layers = [self.eq, self.ek]
            qk_input_layers = []

        for m in qk_output_layers:
            if mode == "zero":
                nn.init.zeros_(m.weight)
            else:
                nn.init.normal_(m.weight, 0.0, scale)

        for m in qk_input_layers:
            if mode == "zero":
                nn.init.kaiming_uniform_(m.weight, a=5**0.5)
            else:
                nn.init.normal_(m.weight, 0.0, scale)

        if self.enable_cam_residual:
            v_modules = [self.ev[0], self.ev[2]] if self.use_mlp else [self.ev]
            for m in v_modules:
                nn.init.normal_(m.weight, 0.0, scale)
            if self.use_mlp:
                nn.init.xavier_uniform_(self.gate_proj[0].weight)
                nn.init.zeros_(self.gate_proj[0].bias)
                nn.init.zeros_(self.gate_proj[2].weight)
            else:
                nn.init.zeros_(self.gate_proj.weight)

    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    # Plรผcker decomposition
    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    @staticmethod
    def decompose_plucker(plucker_6d: torch.Tensor):
        """Decompose (d, m) โ†’ (d, mฬ‚, logโ€–mโ€–).



        Returns:

            feat_q: (B, S, 7) = (d, mฬ‚, log_s) for Q projection.

            feat_k: (B, S, 7) = (mฬ‚, d, log_s) for K projection (flip).

            log_scale: (B, S, 1) for scale gate.

        """
        d = plucker_6d[..., :3]
        m = plucker_6d[..., 3:]

        m_norm = m.norm(dim=-1, keepdim=True).clamp(min=1e-6)
        m_hat = m / m_norm
        log_scale = torch.log(m_norm)

        feat_q = torch.cat([d, m_hat, log_scale], dim=-1)
        feat_k = torch.cat([m_hat, d, log_scale], dim=-1)  # flip d โ†” mฬ‚
        return feat_q, feat_k, log_scale

    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    # Training-time scale augmentation
    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    def _maybe_perturb_log_scale(self, log_scale: torch.Tensor) -> torch.Tensor:
        """Apply a per-sample uniform shift to log_scale during training.



        The shift is shared across all tokens of a sample (same offset for

        all frames / patches), mimicking the effect of globally rescaling

        the camera translation (e.g. `poses[:, :, 3] *= k` โ†’ log_scale += log k).



        Only the copy fed into `scale_gate` is perturbed; E_q / E_k still see

        the true log_scale so absolute-distance information is preserved.



        No-op when:

          * not training, or

          * `log_scale_aug_prob <= 0`, or

          * the Bernoulli draw rejects this forward.

        """
        if not self.training or self.log_scale_aug_prob <= 0.0:
            return log_scale
        # Bernoulli(prob) gate โ€” batch-wide single draw to minimise overhead.
        if torch.rand((), device=log_scale.device).item() > self.log_scale_aug_prob:
            return log_scale
        lo, hi = self.log_scale_aug_range
        B = log_scale.shape[0]
        # (B, 1, 1) broadcast over (S, 1) โ†’ per-sample scalar shift.
        shift = torch.empty(B, 1, 1, device=log_scale.device, dtype=log_scale.dtype).uniform_(
            lo, hi
        )
        return log_scale + shift

    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    # Core forward: Q/K only
    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    def apply_to_qk(

        self,

        q: torch.Tensor,

        k: torch.Tensor,

        plucker_6d: torch.Tensor,

    ):
        """Add scale-gated Plรผcker PE to Q and K.



        Args:

            q: (B, S, D) query after RoPE.

            k: (B, S, D) key after RoPE.

            plucker_6d: (B, S, 6) raw Plรผcker coordinates (d, m).



        Returns:

            q, k with Normalize-Gate-Inject PE applied.

        """
        feat_q, feat_k, log_scale = self.decompose_plucker(plucker_6d)

        pe_q = self.norm_pe_q(self.eq(feat_q.to(q.dtype)))
        pe_k = self.norm_pe_k(self.ek(feat_k.to(k.dtype)))

        # Perturb the gate's log_scale input only โ€” feat_q/feat_k keep the
        # true log_scale so E_q / E_k absolute-distance information stays intact.
        log_scale_for_gate = self._maybe_perturb_log_scale(log_scale)
        gate = self.scale_gate(log_scale_for_gate.to(q.dtype))  # (B, S, D)
        pe_q = gate * pe_q
        pe_k = gate * pe_k

        if self.use_scale:
            pe_q = self.alpha_q * pe_q
            pe_k = self.alpha_k * pe_k

        return q + pe_q, k + pe_k

    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    # Extended forward: Q/K + cam_residual
    # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    def apply_to_qk_and_output(

        self,

        q: torch.Tensor,

        k: torch.Tensor,

        plucker_6d: torch.Tensor,

        num_frames: int = 1,

    ):
        """Apply Plรผcker PE to Q/K, optionally compute frame-uniform cam_residual.



        Args:

            q, k: (B, S, dim) where S = num_frames * H * W.

            plucker_6d: (B, S, 6) raw Plรผcker coordinates.

            num_frames: latent frame count T, for frame-level averaging.



        Returns:

            q, k: with Normalize-Gate-Inject PE applied.

            cam_residual: (B, S, dim) or None.

        """
        orig_dtype = q.dtype
        feat_q, feat_k, log_scale = self.decompose_plucker(plucker_6d)
        feat_q = feat_q.to(orig_dtype)
        feat_k = feat_k.to(orig_dtype)

        pe_q = self.norm_pe_q(self.eq(feat_q))
        pe_k = self.norm_pe_k(self.ek(feat_k))

        # Perturb the gate's log_scale input only โ€” feat_q/feat_k keep the
        # true log_scale so E_q / E_k absolute-distance information stays intact.
        log_scale_for_gate = self._maybe_perturb_log_scale(log_scale)
        gate = self.scale_gate(log_scale_for_gate.to(orig_dtype))
        pe_q = gate * pe_q
        pe_k = gate * pe_k

        if self.use_scale:
            pe_q = self.alpha_q * pe_q
            pe_k = self.alpha_k * pe_k

        q = q + pe_q
        k = k + pe_k

        cam_residual = None
        if self.enable_cam_residual:
            B, S, C = feat_q.shape
            spatial = S // num_frames
            # frame-level average of normalized features
            feat_frame = feat_q.reshape(B, num_frames, spatial, C).mean(dim=2, keepdim=True)
            feat_frame = feat_frame.expand(B, num_frames, spatial, C).reshape(B, S, C)

            cam_gate = torch.sigmoid(self.gate_logit + self.gate_proj(feat_frame))
            cam_residual = cam_gate.to(orig_dtype) * self.ev(feat_frame)

        return q.to(orig_dtype), k.to(orig_dtype), cam_residual