File size: 19,102 Bytes
c335050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
"""
GPU test: how many Wan 2.1 1.3B weights does the CGLA DiT block load?

Builds a Wan-2.1-T2V-1.3B-shaped DiT whose every ``DiTBlock`` is replaced by
the CGLA block (``diffsynth/models/memory/u_vit_cgla_blocks.py::
CGLATransformerBlock`` β€” the real DFOT SSE-GLA block, sequential attn+mlp,
carrying Wan-shaped submodules ``cross_attn``/``norm1/2/3``/``ffn``/
``modulation``/``gate``). Loads the Wan 2.1 1.3B checkpoint and reports:

  * which block submodule prefixes loaded successfully (Wan key -> CGLA param)
  * which were NOT loaded, split into:
      - "unexpected" (Wan has them, CGLA block does not β€” e.g. Wan's softmax
        ``self_attn``), and
      - "missing"    (CGLA block has them, Wan does not β€” the new SSE-GLA
        attention ``spatial_*``/``temporal_attn``/``noise_write_gate``/``mlp_*``)
  * non-block Wan keys (patch_embedding / text_embedding / time_embedding /
    final_layer / ...) reported separately (not part of the DiTBlock swap)
  * coverage numbers: #keys, #params, % of Wan block params loaded, % of CGLA
    block params that are Wan-initialised.

Wan 2.1 T2V 1.3B config (``diffsynth/models/wan_video_dit.py`` hash
``9269f8db...``): dim=1536, num_heads=12, num_layers=30, ffn_dim=8960,
has_image_input=False, eps=1e-6, patch_size=(1,2,2). These are auto-detected
from the checkpoint where possible (dim/ffn_dim/num_layers/has_image_input);
``num_heads`` is not weight-inferable and uses the known config (12).

NOTE: this is a weight-LOADING test (no forward pass). Run on a GPU node that
has the Wan base model + the echo-memory env:

    PYTHONPATH=. python3 tests/test_cgla_wan.py
    # optional: --ckpt /path/to/diffusion_pytorch_model.safetensors
    # optional: --mechanism prope|ucpe   (loading result is identical across
    #                                     mechanisms; only new params differ)
"""

from __future__ import annotations

import os
import sys
from collections import Counter, defaultdict

_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO not in sys.path:
    sys.path.insert(0, _REPO)

import torch
import torch.nn as nn
from safetensors.torch import load_file as safe_load_file

from diffsynth.models.memory.u_vit_cgla_blocks import CGLATransformerBlock, remap_wan_to_cgla

# ── Wan 2.1 T2V 1.3B config (diffsynth/models/wan_video_dit.py, hash 9269f8db…) ──
WAN_T2V_1_3B = dict(
    dim=1536,
    num_heads=12,            # not weight-inferable; from the Wan config
    num_layers=30,
    ffn_dim=8960,
    has_image_input=False,
    eps=1e-6,
    patch_size=(1, 2, 2),
)

# CGLA block forward-only shape params (do not affect weight shapes/loading):
#   640x352 frame -> VAE /8 -> 80x44 -> DiT patchify /2 -> 40x22 = 880 patches;
#   81 frames -> VAE /4 (+1) -> 21 latent frames.
CGLA_NUM_PATCHES = 880
CGLA_TEMPORAL_LENGTH = 21
CGLA_EMB_DIM = 1024       # dfot NormalizeWithCond FiLM emb_dim (CGLA-new param)
CGLA_POSE_DIM = 12        # per-frame RT camera pose
CGLA_HEAD_DIM = 128       # = dim/num_heads = 1536/12 -> SSEGLA key_dim == dim

DEFAULT_CKPT = "/apdcephfs_zwfy/share_303204533/jiakuihu/checkpoints/Wan2.1-T2V-1.3B/diffusion_pytorch_model.safetensors"


def detect_config(sd: dict) -> dict:
    """Auto-detect dim / ffn_dim / num_layers / has_image_input from the ckpt."""
    cfg = dict(WAN_T2V_1_3B)
    # num_layers: distinct blocks.<N>. prefixes.
    blk_ids = sorted({int(k.split(".")[1]) for k in sd if k.startswith("blocks.")})
    if blk_ids:
        cfg["num_layers"] = len(blk_ids)
    # dim / ffn_dim / has_image_input from block 0 weight shapes.
    def _shape(name):
        return tuple(sd[name].shape) if name in sd else None
    q = _shape("blocks.0.self_attn.q.weight") or _shape("blocks.0.cross_attn.q.weight")
    if q is not None:
        cfg["dim"] = q[0]
    ffn0 = _shape("blocks.0.ffn.0.weight")
    if ffn0 is not None:
        cfg["ffn_dim"] = ffn0[0]
    cfg["has_image_input"] = "blocks.0.cross_attn.k_img.weight" in sd
    return cfg


def build_cgla_dit(cfg: dict, mechanism: str) -> nn.Module:
    """A Wan-shaped DiT whose blocks are CGLATransformerBlock (no VAE/text).

    CGLATransformerBlock is a Wan DiTBlock with CGLA (SSE-GLA) replacing the
    self-attention (linear attention on the full flattened token sequence), then
    cross-attention to text, then FFN. Wan submodules (cross_attn/norm1/2/3/
    ffn/modulation/gate) + the SSE-GLA q/k/v/o projections load from the Wan
    checkpoint; the rest of the SSE-GLA params are new.
    """
    use_pose_rope = mechanism in ("prope", "ucpe")
    # head_dim = dim // num_heads so SSE-GLA key_dim == dim (Wan's q/k/v/o are
    # Linear(dim, dim), loadable into the linear-attention projections). For
    # real Wan 1.3B this is 1536 // 12 = 128 (== CGLA_HEAD_DIM).
    head_dim = cfg["dim"] // cfg["num_heads"]

    class CGLADiT(nn.Module):
        def __init__(self):
            super().__init__()
            self.blocks = nn.ModuleList([
                CGLATransformerBlock(
                    has_image_input=cfg["has_image_input"],
                    dim=cfg["dim"],
                    num_heads=cfg["num_heads"],
                    ffn_dim=cfg["ffn_dim"],
                    eps=cfg["eps"],
                    head_dim=head_dim,
                    num_sparse_partition=4,
                    num_writer=1,
                    num_reader=1,
                    pose_dim=CGLA_POSE_DIM,
                    pose_bottleneck=64,
                    gate_logit_normalizer=16,
                    gate_low_rank_dim=16,
                    use_pose_rope=use_pose_rope,
                    use_pose_gate_mod=False,
                    layer_idx=i,
                    emb_dim=CGLA_EMB_DIM,
                )
                for i in range(cfg["num_layers"])
            ])

    return CGLADiT()


def _top(prefix_key: str) -> str:
    """Block submodule prefix: 'blocks.N.<attr>...' -> '<attr>';
    non-block key -> its first component ('patch_embedding.weight' -> 'patch_embedding')."""
    if prefix_key.startswith("blocks."):
        parts = prefix_key.split(".")  # ["blocks", "<N>", "<attr>", ...]
        return parts[2] if len(parts) > 2 else parts[-1]
    return prefix_key.split(".")[0]


def _numel(sd, keys):
    return int(sum(sd[k].numel() for k in keys if k in sd))


def _prefix_summary(keys):
    """Group keys by their first component; return {prefix: (n_keys, n_params)}."""
    # keys here are full ckpt keys (blocks.N.<rest>) β€” group by <rest>[0].
    g = defaultdict(list)
    for k in keys:
        rest = k.split(".", 2)[2] if k.startswith("blocks.") else k
        g[rest.split(".")[0]].append(k)
    return g


def report(sd, model, cfg, mechanism):
    n_blk = cfg["num_layers"]

    # Remap Wan self-attn keys (self_attn.{q,k,v,o}) to CGLA SSE-GLA names
    # (self_attn.{q,k,v,o}_proj) so Wan's softmax-attn weights initialise the
    # linear-attention projections. After remap, "loaded" reflects this.
    sd = remap_wan_to_cgla(sd)

    model_sd = model.state_dict()
    ckpt_keys = set(sd.keys())
    model_keys = set(model_sd.keys())

    # Block-only keys (the DiTBlock swap domain).
    ckpt_blk = {k for k in ckpt_keys if k.startswith("blocks.")}
    model_blk = {k for k in model_keys if k.startswith("blocks.")}
    # Non-block Wan keys (patch_embedding / text_embedding / time_embedding / ...).
    ckpt_nonblk = ckpt_keys - ckpt_blk

    missing, unexpected = model.load_state_dict(sd, strict=False)
    # Restrict to block keys for the swap analysis (non-block keys are reported
    # separately as "not part of the DiTBlock replacement").
    missing_blk = [k for k in missing if k.startswith("blocks.")]
    unexpected_blk = [k for k in unexpected if k.startswith("blocks.")]
    unexpected_nonblk = [k for k in unexpected if not k.startswith("blocks.") and k in ckpt_nonblk]

    loaded_blk = sorted(model_blk - set(missing_blk))  # in both -> loaded

    # ── param tallies ──────────────────────────────────────────────────────
    wan_blk_params = _numel(sd, ckpt_blk)
    loaded_params = _numel(sd, loaded_blk)
    cgla_blk_params = _numel(model_sd, model_blk)
    wan_init_params = loaded_params

    # ── per-prefix grouping (over one block, Γ— num_layers) ─────────────────
    def _per_prefix(keys, src):
        g = defaultdict(lambda: [0, 0])  # prefix -> [n_keys, n_params]
        for k in keys:
            pref = _top(k)
            g[pref][0] += 1
            g[pref][1] += int(src[k].numel()) if k in src else 0
        return g

    loaded_g = _per_prefix(loaded_blk, sd)
    miss_g = _per_prefix(missing_blk, model_sd)
    unexp_g = _per_prefix(unexpected_blk, sd)
    nonblk_g = _per_prefix(ckpt_nonblk, sd)

    # ── print ──────────────────────────────────────────────────────────────
    print("=" * 78)
    print("WAN 2.1 1.3B -> CGLA DiT  weight-loading report")
    print("=" * 78)
    print(f"checkpoint          : {CKPT}")
    print(f"detected Wan config : {cfg}")
    print(f"CGLA mechanism      : {mechanism}")
    print(f"  (use_pose_rope={mechanism in ('prope','ucpe')}; loading coverage is")
    print(f"   identical across cgla/prope/ucpe β€” only *new* (non-Wan) params differ)")
    print(f"num blocks          : {n_blk}")
    print(f"head_dim (SSEGLA)   : {CGLA_HEAD_DIM}  (= dim/num_heads -> SSEGLA key_dim == dim)")
    print()

    print("-" * 78)
    print("1) SUCCESSFULLY LOADED  (Wan block key -> CGLA param, name+shape match)")
    print("-" * 78)
    if loaded_g:
        print(f"  {'prefix':<22}{'keys (Γ—N blk)':>16}{'params':>16}")
        for pref in sorted(loaded_g):
            n, p = loaded_g[pref]
            print(f"  {pref:<22}{n:>16}{p:>16,}")
    print(f"  {'(total loaded)':<22}{'':>16}{loaded_params:>16,}")
    print()

    print("-" * 78)
    print("2a) NOT LOADED β€” unexpected  (in Wan ckpt, NOT in CGLA block)")
    print("-" * 78)
    if unexp_g:
        print(f"  {'prefix':<22}{'keys (Γ—N blk)':>16}{'params':>16}")
        for pref in sorted(unexp_g):
            n, p = unexp_g[pref]
            print(f"  {pref:<22}{n:>16}{p:>16,}")
        print(f"  {'(total unexpected)':<22}{'':>16}{_numel(sd, unexpected_blk):>16,}")
    else:
        print("  (none)")
    print()

    print("-" * 78)
    print("2b) NOT LOADED β€” missing  (CGLA block has, NOT in Wan ckpt = new CGLA params)")
    print("-" * 78)
    if miss_g:
        print(f"  {'prefix':<22}{'keys (Γ—N blk)':>16}{'params':>16}")
        for pref in sorted(miss_g):
            n, p = miss_g[pref]
            print(f"  {pref:<22}{n:>16}{p:>16,}")
        print(f"  {'(total missing/new)':<22}{'':>16}{_numel(model_sd, missing_blk):>16,}")
    else:
        print("  (none)")
    print()

    print("-" * 78)
    print("3) NON-BLOCK Wan keys  (not part of the DiTBlock swap; e.g. patch/text")
    print("   /time/final embeddings β€” the CGLA model has no such submodules)")
    print("-" * 78)
    if nonblk_g:
        print(f"  {'prefix':<22}{'keys':>10}{'params':>16}")
        for pref in sorted(nonblk_g):
            n, p = nonblk_g[pref]
            print(f"  {pref:<22}{n:>10}{p:>16,}")
    else:
        print("  (none)")
    print()

    # ── coverage summary ───────────────────────────────────────────────────
    wan_total = sum(v.numel() for v in sd.values())
    pct_wan_loaded = 100.0 * loaded_params / wan_blk_params if wan_blk_params else 0.0
    pct_cgla_waninit = 100.0 * wan_init_params / cgla_blk_params if cgla_blk_params else 0.0
    print("=" * 78)
    print("COVERAGE SUMMARY")
    print("=" * 78)
    print(f"  Wan ckpt total params              : {wan_total:>14,}")
    print(f"  Wan BLOCK params (blocks.*.*       ) : {wan_blk_params:>14,}")
    print(f"  -> loaded into CGLA block          : {loaded_params:>14,}  ({pct_wan_loaded:5.2f}% of Wan block params)")
    print(f"  CGLA block params (new + Wan-shape): {cgla_blk_params:>14,}")
    print(f"  -> Wan-initialised (loaded)         : {wan_init_params:>14,}  ({pct_cgla_waninit:5.2f}% of CGLA block params)")
    print(f"  -> CGLA-new (missing, needs training): {cgla_blk_params - wan_init_params:>14,}  "
          f"({100.0*(cgla_blk_params-wan_init_params)/cgla_blk_params:5.2f}% of CGLA block params)")
    print("=" * 78)

    _report_ssegla_param_audit(model, sd, mechanism)


def _self_attn_subparam(key: str):
    """For 'blocks.N.self_attn.<sub>.[...]' -> '<sub>'; else None."""
    parts = key.split(".")
    if len(parts) >= 3 and parts[0] == "blocks" and parts[2] == "self_attn":
        return parts[3] if len(parts) > 3 else None
    return None


def _report_ssegla_param_audit(model, sd, mechanism):
    """Per-param audit of the SSE-GLA ``self_attn`` submodules.

    Answers: for each SSEGLA param (q_proj / k_proj / v_proj / o_proj /
    lora_q_proj / lora_k_proj / gk_proj / e_proj / g_proj / o_norm) β€” LOADED from
    Wan (name+shape match via the remap) or REINIT (no Wan key)? And for the
    PRoPE/UCPE pose modules (pose_encoder / pose_q_proj / pose_k_proj /
    pose_gk_proj / pose_e_proj / pose_rope / pose_gate_mod) β€” all REINIT, with
    their dfot zero-init policy noted (the 'up' projections are zero-init; the
    'down' pose_encoder is default-init).
    """
    model_sd = model.state_dict()
    # Group by self_attn subparam over all blocks.
    def _group(keys, src):
        g = defaultdict(lambda: [0, 0])  # subparam -> [n_keys, n_params]
        for k in keys:
            sp = _self_attn_subparam(k)
            if sp is None:
                continue
            g[sp][0] += 1
            g[sp][1] += int(src[k].numel()) if k in src else 0
        return g

    model_sub = _group(model_sd.keys(), model_sd)      # CGLA block params
    ckpt_sub = _group(sd.keys(), sd)                   # Wan (remapped) params

    # Wan self_attn projection names that the remap targets.
    WAN_REMAP_TARGETS = {"q_proj", "k_proj", "v_proj", "o_proj"}
    # dfot pose 'up'-projection zero-init (see fla/layers/sse.py:352-355, 359-360;
    # PoseRoPE net[-1] zero-init at sse.py:98-99).
    POSE_ZERO_UP = {
        "pose_q_proj", "pose_k_proj", "pose_gk_proj", "pose_e_proj",
        "pose_gate_mod", "pose_rope",
    }

    print()
    print("=" * 78)
    print("4) SSE-GLA self_attn  PARAM-BY-PARAM  (loaded vs reinitialised)")
    print("=" * 78)
    print(f"  {'SSEGLA param':<16}{'Wan source':<20}{'keys (Γ—N blk)':>14}{'params':>14}  status")
    print("  " + "-" * 74)
    total_loaded_sa = 0
    total_reinit_sa = 0
    for sp in sorted(model_sub):
        n, p = model_sub[sp]
        wan_src = "β€”"
        status = "REINIT (no Wan key)"
        if sp in WAN_REMAP_TARGETS:
            wan_name = sp.replace("_proj", "")   # q_proj -> q
            wan_src = f"self_attn.{wan_name}"
            if sp in ckpt_sub:
                status = "LOADED"
                total_loaded_sa += p
            else:
                status = "REINIT (Wan key absent)"
                total_reinit_sa += p
        else:
            total_reinit_sa += p
        note = ""
        if sp in POSE_ZERO_UP and mechanism in ("prope", "ucpe"):
            note = " [dfot zero-init 'up' proj]"
        elif sp == "pose_encoder" and mechanism in ("prope", "ucpe"):
            note = " [dfot default-init 'down' proj]"
        print(f"  {sp:<16}{wan_src:<20}{n:>14}{p:>14,}  {status}{note}")
    print("  " + "-" * 74)
    print(f"  self_attn loaded   (q/k/v/o_proj from Wan): {total_loaded_sa:>14,}")
    print(f"  self_attn reinit   (GLA gates/LoRA/norm):    {total_reinit_sa:>14,}")
    print()

    # Pose modules exist only for PRoPE / UCPE.
    print("-" * 78)
    if mechanism in ("prope", "ucpe"):
        print(f"PRoPE/UCPE pose modules (use_pose_rope=True) β€” ALL reinitialised:")
        print(f"  {'pose module':<16}{'init policy (dfot)':<40}{'params':>14}")
        print("  " + "-" * 70)
        pose_rows = [
            ("pose_encoder",  "default-init (down proj; NOT zeroed)"),
            ("pose_q_proj",   "zero-init 'up' proj  => pose contributes 0 at step 0"),
            ("pose_k_proj",   "zero-init 'up' proj"),
            ("pose_gk_proj",  "zero-init 'up' proj"),
            ("pose_e_proj",   "zero-init (direct)"),
            ("pose_rope",     "PoseRoPE net[-1] zero-init (angle MLP)"),
        ]
        if any(_self_attn_subparam(k) == "pose_gate_mod" for k in model_sd):
            pose_rows.append(("pose_gate_mod", "zero-init (weight+bias)"))
        for name, policy in pose_rows:
            p = model_sub.get(name, [0, 0])[1]
            present = "present" if name in model_sub else "ABSENT"
            print(f"  {name:<16}{policy:<40}{p:>14,}  {present}")
        print("  => at step 0, pose contributes exactly 0 to q2/k2/gk2/eta (LoRA-style),")
        print("     so PRoPE/UCPE is numerically identical to vanilla CGLA until trained.")
    else:
        print(f"mechanism={mechanism!r}: no pose modules (use_pose_rope=False).")
    print("=" * 78)


def parse_args():
    import argparse
    p = argparse.ArgumentParser(description="CGLA <-> Wan 2.1 1.3B weight-loading test")
    p.add_argument("--ckpt", default=DEFAULT_CKPT,
                   help="path to diffusion_pytorch_model.safetensors")
    p.add_argument("--mechanism", default="prope", choices=["cgla", "prope", "ucpe"],
                   help="CGLA variant (loading coverage is identical across them)")
    return p.parse_args()


CKPT = DEFAULT_CKPT  # set in main() from args


def main():
    global CKPT
    args = parse_args()
    CKPT = args.ckpt
    if not os.path.isfile(CKPT):
        print(f"ERROR: checkpoint not found: {CKPT}", file=sys.stderr)
        print("  set --ckpt /path/to/Wan2.1-T2V-1.3B/diffusion_pytorch_model.safetensors",
              file=sys.stderr)
        sys.exit(1)

    device = "cuda" if torch.cuda.is_available() else "cpu"
    print(f"[cgla_wan] loading checkpoint (CPU): {CKPT}")
    sd = safe_load_file(CKPT)
    cfg = detect_config(sd)
    print(f"[cgla_wan] detected config: {cfg}")

    # Build the CGLA DiT (CPU β€” loading does not need GPU) and load Wan weights.
    model = build_cgla_dit(cfg, args.mechanism)
    report(sd, model, cfg, args.mechanism)

    # Confirm the CGLA block constructs on GPU (fla/triton compile at forward,
    # not at construction; this just moves params to cuda).
    if device == "cuda":
        try:
            model = model.to(device=device, dtype=torch.bfloat16)
            n = sum(p.numel() for p in model.parameters())
            print(f"[cgla_wan] CGLA DiT moved to {device} (bf16); {n:,} params construct OK")
        except Exception as e:
            print(f"[cgla_wan] WARN: GPU move failed: {e}")
    else:
        print("[cgla_wan] no CUDA β€” skipping GPU construct check")


if __name__ == "__main__":
    main()