yujiepan commited on
Commit
2c1a8e3
·
verified ·
1 Parent(s): 31635d8

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
.meta/create_model.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def create_model(source_model_id: str, source_revision: str, save_folder: str) -> str:
2
+ import copy
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ from huggingface_hub import hf_hub_download
8
+ from safetensors.torch import save_file
9
+ from transformers import AutoConfig, AutoModelForImageTextToText, set_seed
10
+
11
+ folder = Path(save_folder)
12
+ folder.mkdir(parents=True, exist_ok=True)
13
+ # Transformers 5.18 loads this custom Loop/mHC wrapper around the native Qwen3.5 backbone; ship it pinned.
14
+ for name in (
15
+ "modeling_sensenova_loop.py",
16
+ "chat_template.jinja",
17
+ "processor_config.json",
18
+ "preprocessor_config.json",
19
+ "video_preprocessor_config.json",
20
+ "tokenizer.json",
21
+ "tokenizer_config.json",
22
+ ):
23
+ (folder / name).write_bytes(Path(hf_hub_download(source_model_id, name, revision=source_revision)).read_bytes())
24
+
25
+ config = json.loads(Path(hf_hub_download(source_model_id, "config.json", revision=source_revision)).read_text())
26
+ # Pre-loop 0-3, looped block 4-7 executed twice, post-loop 8-11: twelve physical layers / sixteen executions,
27
+ # four complete linear/linear/linear/full cycles. Residual width 8, head dim 32 with eight Q / four KV heads.
28
+ text_config = config["text_config"]
29
+ text_config.update(
30
+ head_dim=32,
31
+ hidden_size=8,
32
+ intermediate_size=32,
33
+ num_hidden_layers=12,
34
+ num_attention_heads=8,
35
+ num_key_value_heads=4,
36
+ linear_key_head_dim=32,
37
+ linear_num_key_heads=4,
38
+ linear_num_value_heads=8,
39
+ linear_value_head_dim=32,
40
+ )
41
+ text_config["layer_types"] = ["linear_attention", "linear_attention", "linear_attention", "full_attention"] * 3
42
+ text_config["rope_parameters"]["mrope_section"] = [1, 1, 2]
43
+ # Two-stream mHC spans exactly the looped block [4, 8), matching the source [4, 28) cadence.
44
+ config["loop_settings"] = {"counts": [2], "end": 8, "eval_count": 2, "start": 4}
45
+ config["mhc"] = {
46
+ "end": 8,
47
+ "fixed_identity": False,
48
+ "iterations": 20,
49
+ "pre_scale": 1.0,
50
+ "start": 4,
51
+ "streams": 2,
52
+ }
53
+ config["hidden_size"] = 8
54
+ # Four vision blocks keep the native Qwen3.5 tower and the fixed patch/temporal/merge and position contracts.
55
+ config["vision_config"].update(
56
+ hidden_size=64,
57
+ intermediate_size=128,
58
+ num_heads=2,
59
+ out_hidden_size=8,
60
+ depth=4,
61
+ )
62
+ (folder / "config.json").write_text(json.dumps(config, indent=2) + "\n")
63
+
64
+ set_seed(42)
65
+ model = AutoModelForImageTextToText.from_config(
66
+ AutoConfig.from_pretrained(folder, trust_remote_code=True),
67
+ trust_remote_code=True,
68
+ dtype=torch.bfloat16,
69
+ )
70
+ # The source keeps the hybrid linear-attention SSM parameters FP32 even under a BF16 load.
71
+ from transformers.models.qwen3_5.modeling_qwen3_5 import Qwen3_5GatedDeltaNet
72
+
73
+ for layer in model.model.language_model.layers:
74
+ linear = getattr(layer, "linear_attn", None)
75
+ if isinstance(linear, Qwen3_5GatedDeltaNet):
76
+ with torch.no_grad():
77
+ linear.A_log = torch.nn.Parameter(linear.A_log.float())
78
+ linear.dt_bias = torch.nn.Parameter(linear.dt_bias.float())
79
+ linear.norm.weight = torch.nn.Parameter(linear.norm.weight.float())
80
+ linear.conv1d.weight = torch.nn.Parameter(linear.conv1d.weight.float())
81
+ # One BF16 MTP draft layer reusing a full-attention backbone layer outside the mHC range.
82
+ full_layer = model.model.language_model.layers[11]
83
+ model.mtp = torch.nn.ModuleDict(
84
+ {
85
+ "pre_fc_norm_embedding": torch.nn.RMSNorm(8),
86
+ "fc": torch.nn.Linear(16, 8, bias=False),
87
+ "layers": torch.nn.ModuleList([copy.deepcopy(full_layer)]),
88
+ "norm": torch.nn.RMSNorm(8),
89
+ "pre_fc_norm_hidden": torch.nn.RMSNorm(8),
90
+ }
91
+ )
92
+ model = model.cpu()
93
+ representation = str(model)
94
+
95
+ state = model.state_dict()
96
+ backbone, draft = {}, {}
97
+ for name, tensor in state.items():
98
+ if name.startswith("mtp."):
99
+ draft[name] = tensor.to(torch.bfloat16).contiguous()
100
+ else:
101
+ # The shipped checkpoint stores the complete backbone FP32, mHC routers included.
102
+ backbone[name] = tensor.to(torch.float32).contiguous()
103
+ save_file(backbone, str(folder / "model.safetensors"), metadata={"format": "pt"})
104
+ save_file(draft, str(folder / "mtp.safetensors"), metadata={"format": "pt"})
105
+ # Rewrite the edited config (save was manual) and pin the source generation config bytes.
106
+ (folder / "config.json").write_text(json.dumps(config, indent=2) + "\n")
107
+ (folder / "generation_config.json").write_bytes(
108
+ Path(hf_hub_download(source_model_id, "generation_config.json", revision=source_revision)).read_bytes()
109
+ )
110
+
111
+ expected_draft = {
112
+ "mtp.fc.weight",
113
+ "mtp.pre_fc_norm_embedding.weight",
114
+ "mtp.pre_fc_norm_hidden.weight",
115
+ "mtp.norm.weight",
116
+ "mtp.layers.0.input_layernorm.weight",
117
+ "mtp.layers.0.post_attention_layernorm.weight",
118
+ "mtp.layers.0.self_attn.q_proj.weight",
119
+ "mtp.layers.0.self_attn.k_proj.weight",
120
+ "mtp.layers.0.self_attn.v_proj.weight",
121
+ "mtp.layers.0.self_attn.o_proj.weight",
122
+ "mtp.layers.0.self_attn.q_norm.weight",
123
+ "mtp.layers.0.self_attn.k_norm.weight",
124
+ "mtp.layers.0.mlp.gate_proj.weight",
125
+ "mtp.layers.0.mlp.up_proj.weight",
126
+ "mtp.layers.0.mlp.down_proj.weight",
127
+ }
128
+ assert set(draft) == expected_draft, sorted(set(draft) ^ expected_draft)
129
+ assert all(
130
+ name.count("mhc_attn") + name.count("mhc_ffn") == 0 or backbone[name].dtype == torch.float32
131
+ for name in backbone
132
+ )
133
+ return representation
134
+
135
+
136
+ create_model(
137
+ source_model_id="sensenova/SenseNova-Loop-9B-Preview",
138
+ source_revision="9937e91c7301b7dcbb5bf17e3e4e95c499a11e99",
139
+ save_folder="/tmp/sensenova-loop",
140
+ )
.meta/env.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "source_model": {
3
+ "name": "sensenova/SenseNova-Loop-9B-Preview",
4
+ "commit_id": "9937e91c7301b7dcbb5bf17e3e4e95c499a11e99"
5
+ },
6
+ "environments": {
7
+ "creation": {
8
+ "python": "3.12.13",
9
+ "packages": {
10
+ "huggingface_hub": "2.3.0",
11
+ "numpy": "2.5.2",
12
+ "pillow": "12.3.0",
13
+ "safetensors": "0.8.0",
14
+ "tokenizers": "0.23.3",
15
+ "torch": "2.13.0+cu126",
16
+ "transformers": "5.22.0.dev0"
17
+ },
18
+ "cuda": "12.6",
19
+ "hardware": {
20
+ "gpu_model": "NVIDIA H20"
21
+ }
22
+ },
23
+ "transformers": {
24
+ "python": "3.12.13",
25
+ "packages": {
26
+ "huggingface_hub": "2.3.0",
27
+ "numpy": "2.5.2",
28
+ "pillow": "12.3.0",
29
+ "safetensors": "0.8.0",
30
+ "tokenizers": "0.23.3",
31
+ "torch": "2.13.0+cu126",
32
+ "transformers": "5.22.0.dev0",
33
+ "triton": "3.7.1"
34
+ },
35
+ "cuda": "12.6",
36
+ "hardware": {
37
+ "gpu_model": "NVIDIA H20"
38
+ }
39
+ }
40
+ }
41
+ }
.meta/model.log ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SenseNovaLoopForConditionalGeneration(
2
+ (model): Qwen3_5Model(
3
+ (visual): Qwen3_5VisionModel(
4
+ (patch_embed): Qwen3_5VisionPatchEmbed(
5
+ (proj): Conv3d(3, 64, kernel_size=(2, 16, 16), stride=(2, 16, 16))
6
+ )
7
+ (pos_embed): Embedding(2304, 64)
8
+ (rotary_pos_emb): Qwen3_5VisionRotaryEmbedding()
9
+ (blocks): ModuleList(
10
+ (0-3): 4 x Qwen3_5VisionBlock(
11
+ (norm1): LayerNorm((64,), eps=1e-06, elementwise_affine=True, bias=True)
12
+ (norm2): LayerNorm((64,), eps=1e-06, elementwise_affine=True, bias=True)
13
+ (attn): Qwen3_5VisionAttention(
14
+ (qkv): Linear(in_features=64, out_features=192, bias=True)
15
+ (proj): Linear(in_features=64, out_features=64, bias=True)
16
+ )
17
+ (mlp): Qwen3_5VisionMLP(
18
+ (linear_fc1): Linear(in_features=64, out_features=128, bias=True)
19
+ (linear_fc2): Linear(in_features=128, out_features=64, bias=True)
20
+ (act_fn): GELUTanh()
21
+ )
22
+ )
23
+ )
24
+ (merger): Qwen3_5VisionPatchMerger(
25
+ (norm): LayerNorm((64,), eps=1e-06, elementwise_affine=True, bias=True)
26
+ (linear_fc1): Linear(in_features=256, out_features=256, bias=True)
27
+ (act_fn): GELU(approximate='none')
28
+ (linear_fc2): Linear(in_features=256, out_features=8, bias=True)
29
+ )
30
+ )
31
+ (language_model): SenseNovaLoopTextModel(
32
+ (embed_tokens): Embedding(248320, 8, padding_idx=248044)
33
+ (layers): ModuleList(
34
+ (0-2): 3 x Qwen3_5DecoderLayer(
35
+ (linear_attn): Qwen3_5GatedDeltaNet(
36
+ (conv1d): Conv1d(512, 512, kernel_size=(4,), stride=(1,), padding=(3,), groups=512, bias=False)
37
+ (norm): Qwen3_5RMSNormGated()
38
+ (out_proj): Linear(in_features=256, out_features=8, bias=False)
39
+ (in_proj_qkv): Linear(in_features=8, out_features=512, bias=False)
40
+ (in_proj_z): Linear(in_features=8, out_features=256, bias=False)
41
+ (in_proj_b): Linear(in_features=8, out_features=8, bias=False)
42
+ (in_proj_a): Linear(in_features=8, out_features=8, bias=False)
43
+ )
44
+ (mlp): Qwen3_5MLP(
45
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
46
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
47
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
48
+ (act_fn): SiLUActivation()
49
+ )
50
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
51
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
52
+ )
53
+ (3): Qwen3_5DecoderLayer(
54
+ (self_attn): Qwen3_5Attention(
55
+ (q_proj): Linear(in_features=8, out_features=512, bias=False)
56
+ (k_proj): Linear(in_features=8, out_features=128, bias=False)
57
+ (v_proj): Linear(in_features=8, out_features=128, bias=False)
58
+ (o_proj): Linear(in_features=256, out_features=8, bias=False)
59
+ (q_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
60
+ (k_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
61
+ )
62
+ (mlp): Qwen3_5MLP(
63
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
64
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
65
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
66
+ (act_fn): SiLUActivation()
67
+ )
68
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
69
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
70
+ )
71
+ (4-6): 3 x MHCDecoderLayer(
72
+ (linear_attn): Qwen3_5GatedDeltaNet(
73
+ (conv1d): Conv1d(512, 512, kernel_size=(4,), stride=(1,), padding=(3,), groups=512, bias=False)
74
+ (norm): Qwen3_5RMSNormGated()
75
+ (out_proj): Linear(in_features=256, out_features=8, bias=False)
76
+ (in_proj_qkv): Linear(in_features=8, out_features=512, bias=False)
77
+ (in_proj_z): Linear(in_features=8, out_features=256, bias=False)
78
+ (in_proj_b): Linear(in_features=8, out_features=8, bias=False)
79
+ (in_proj_a): Linear(in_features=8, out_features=8, bias=False)
80
+ )
81
+ (mlp): Qwen3_5MLP(
82
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
83
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
84
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
85
+ (act_fn): SiLUActivation()
86
+ )
87
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
88
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
89
+ (mhc_attn): MHCRouter()
90
+ (mhc_ffn): MHCRouter()
91
+ )
92
+ (7): MHCDecoderLayer(
93
+ (self_attn): Qwen3_5Attention(
94
+ (q_proj): Linear(in_features=8, out_features=512, bias=False)
95
+ (k_proj): Linear(in_features=8, out_features=128, bias=False)
96
+ (v_proj): Linear(in_features=8, out_features=128, bias=False)
97
+ (o_proj): Linear(in_features=256, out_features=8, bias=False)
98
+ (q_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
99
+ (k_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
100
+ )
101
+ (mlp): Qwen3_5MLP(
102
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
103
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
104
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
105
+ (act_fn): SiLUActivation()
106
+ )
107
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
108
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
109
+ (mhc_attn): MHCRouter()
110
+ (mhc_ffn): MHCRouter()
111
+ )
112
+ (8-10): 3 x Qwen3_5DecoderLayer(
113
+ (linear_attn): Qwen3_5GatedDeltaNet(
114
+ (conv1d): Conv1d(512, 512, kernel_size=(4,), stride=(1,), padding=(3,), groups=512, bias=False)
115
+ (norm): Qwen3_5RMSNormGated()
116
+ (out_proj): Linear(in_features=256, out_features=8, bias=False)
117
+ (in_proj_qkv): Linear(in_features=8, out_features=512, bias=False)
118
+ (in_proj_z): Linear(in_features=8, out_features=256, bias=False)
119
+ (in_proj_b): Linear(in_features=8, out_features=8, bias=False)
120
+ (in_proj_a): Linear(in_features=8, out_features=8, bias=False)
121
+ )
122
+ (mlp): Qwen3_5MLP(
123
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
124
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
125
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
126
+ (act_fn): SiLUActivation()
127
+ )
128
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
129
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
130
+ )
131
+ (11): Qwen3_5DecoderLayer(
132
+ (self_attn): Qwen3_5Attention(
133
+ (q_proj): Linear(in_features=8, out_features=512, bias=False)
134
+ (k_proj): Linear(in_features=8, out_features=128, bias=False)
135
+ (v_proj): Linear(in_features=8, out_features=128, bias=False)
136
+ (o_proj): Linear(in_features=256, out_features=8, bias=False)
137
+ (q_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
138
+ (k_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
139
+ )
140
+ (mlp): Qwen3_5MLP(
141
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
142
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
143
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
144
+ (act_fn): SiLUActivation()
145
+ )
146
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
147
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
148
+ )
149
+ )
150
+ (norm): Qwen3_5RMSNorm((8,), eps=1e-06)
151
+ (rotary_emb): Qwen3_5TextRotaryEmbedding()
152
+ )
153
+ )
154
+ (lm_head): Linear(in_features=8, out_features=248320, bias=False)
155
+ (mtp): ModuleDict(
156
+ (pre_fc_norm_embedding): RMSNorm((8,), eps=None, elementwise_affine=True)
157
+ (fc): Linear(in_features=16, out_features=8, bias=False)
158
+ (layers): ModuleList(
159
+ (0): Qwen3_5DecoderLayer(
160
+ (self_attn): Qwen3_5Attention(
161
+ (q_proj): Linear(in_features=8, out_features=512, bias=False)
162
+ (k_proj): Linear(in_features=8, out_features=128, bias=False)
163
+ (v_proj): Linear(in_features=8, out_features=128, bias=False)
164
+ (o_proj): Linear(in_features=256, out_features=8, bias=False)
165
+ (q_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
166
+ (k_norm): Qwen3_5RMSNorm((32,), eps=1e-06)
167
+ )
168
+ (mlp): Qwen3_5MLP(
169
+ (gate_proj): Linear(in_features=8, out_features=32, bias=False)
170
+ (up_proj): Linear(in_features=8, out_features=32, bias=False)
171
+ (down_proj): Linear(in_features=32, out_features=8, bias=False)
172
+ (act_fn): SiLUActivation()
173
+ )
174
+ (input_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
175
+ (post_attention_layernorm): Qwen3_5RMSNorm((8,), eps=1e-06)
176
+ )
177
+ )
178
+ (norm): RMSNorm((8,), eps=None, elementwise_affine=True)
179
+ (pre_fc_norm_hidden): RMSNorm((8,), eps=None, elementwise_affine=True)
180
+ )
181
+ )
README.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ pipeline_tag: image-text-to-text
4
+ base_model:
5
+ - sensenova/SenseNova-Loop-9B-Preview
6
+ ---
7
+
8
+ This is a randomly initialized tiny model based on [sensenova/SenseNova-Loop-9B-Preview](https://huggingface.co/sensenova/SenseNova-Loop-9B-Preview) for testing and debugging.
9
+
10
+ | Feature | Source | This repo |
11
+ |---|---|---|
12
+ | Checkpoint tensor elements | 243.29M BF16, 9.41B F32 | 9.19K BF16, 4.55M F32 |
13
+ | Safetensors weight files | 38.14 GB (9 files) | 18.25 MB (2 files) |
14
+ | Distinct / executed decoder layers | 32 / 56 (block 4-28 twice) | 12 / 16 (block 4-7 twice) |
15
+ | Attention cadence | three linear then full, four-layer cycle | Same; four complete cycles |
16
+ | Hidden width / MLP width | 4,096 / 12,288 | 8 / 32 |
17
+ | Full Q heads / KV heads / head dim | 16 / 4 / 256 | 8 / 4 / 32 |
18
+ | Linear key/value heads x head dim | 16x128 / 32x128 | 4x32 / 8x32 |
19
+ | mHC streams / Sinkhorn iterations | 2 / 20 over layers 4-28 | 2 / 20 over layers 4-7 |
20
+ | MTP draft layers / precision | 1 / BF16 (separate mtp.safetensors) | Same |
21
+ | Vision depth / hidden / heads | 27 / 1,152 / 16 | 4 / 64 / 2 |
22
+ | Patch / temporal patch / spatial merge | 16 / 2 / 2 | Same |
23
+ | Vision position buffer | 2,304 entries | Same |
24
+ | Vocabulary rows / tying | 248,320 / untied | Same |
25
+ | Backbone storage precision | FP32 | Same |
26
+ | SSM/conv/mHC parameters | FP32 | Same |
27
+ | Maximum context length | 262,144 | Same |
28
+
29
+ The pinned upstream Loop/mHC implementation is included for Transformers loading and wraps the native Qwen3.5 backbone; physical layers 4-7 share weights across both visits while every visit owns an independent dynamic-cache slot. The complete backbone (embeddings, language model, vision tower and head) is serialized in FP32 exactly as published; only the standalone MTP draft is BF16. This repo provides a Transformers example with image and video inputs; no serving-backend validation is claimed.
30
+
31
+ | File path | Size |
32
+ |------|------|
33
+ | model.safetensors | 18.2MB |
34
+ | mtp.safetensors | 0.0MB |
35
+
36
+ ### Example usage
37
+
38
+ #### Transformers (✅ verified)
39
+
40
+ Load the included pinned Loop/mHC implementation with `trust_remote_code=True`. The example sends one image and four predecoded video frames; the hybrid linear-attention path requires CUDA.
41
+
42
+ ```python
43
+ def run_transformers(model_id: str):
44
+ import numpy as np
45
+ import torch
46
+ from PIL import Image
47
+ from transformers import AutoModelForImageTextToText, AutoProcessor
48
+ from transformers.video_utils import VideoMetadata
49
+
50
+ model_id = str(model_id)
51
+ processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
52
+ model = (
53
+ AutoModelForImageTextToText.from_pretrained(
54
+ model_id, trust_remote_code=True, dtype=torch.bfloat16, attn_implementation="sdpa"
55
+ )
56
+ .eval()
57
+ .cuda()
58
+ )
59
+ image = Image.fromarray(np.random.randint(0, 255, (256, 256, 3), dtype=np.uint8), "RGB")
60
+ frames = np.stack(
61
+ [
62
+ np.asarray(Image.new("RGB", (256, 256), color))
63
+ for color in ((220, 30, 30), (30, 220, 30), (30, 30, 220), (220, 220, 30))
64
+ ]
65
+ )
66
+ messages = [
67
+ {
68
+ "role": "user",
69
+ "content": [
70
+ {"type": "image", "image": image},
71
+ {"type": "video", "video": frames},
72
+ {"type": "text", "text": "Describe the image and the video."},
73
+ ],
74
+ }
75
+ ]
76
+ inputs = processor.apply_chat_template(
77
+ messages,
78
+ add_generation_prompt=True,
79
+ tokenize=True,
80
+ return_dict=True,
81
+ return_tensors="pt",
82
+ enable_thinking=False,
83
+ processor_kwargs={"video_metadata": [VideoMetadata(total_num_frames=4, fps=2, frames_indices=[0, 1, 2, 3])]},
84
+ ).to(model.device)
85
+ assert inputs["pixel_values"].shape[0] > 0 and inputs["pixel_values_videos"].shape[0] > 0
86
+ with torch.no_grad():
87
+ logits = model(**inputs).logits
88
+ assert torch.isfinite(logits).all()
89
+ output = model.generate(**inputs, do_sample=False, min_new_tokens=8, max_new_tokens=8)
90
+ print(processor.decode(output[0, inputs["input_ids"].shape[1] :], skip_special_tokens=False))
91
+
92
+
93
+ if __name__ == "__main__":
94
+ run_transformers(model_id="scratch/sensenova-loop")
95
+ ```
96
+
97
+ ### Creating this repo
98
+
99
+ - Code: [.meta/create_model.py](.meta/create_model.py)
100
+ - Model structure: [.meta/model.log](.meta/model.log)
101
+ - Environment: [.meta/env.json](.meta/env.json)
102
+
103
+ ### Change log
104
+
105
+ - 2026-11-03: Initial version. Twelve physical decoder layers preserve the pre-loop/loop/post-loop boundaries (layers 0-3, looped 4-7 executed twice, layers 8-11) so the repeated block is one complete four-layer linear/linear/linear/full cycle and the forward runs sixteen layer executions; two-stream mHC with 20 Sinkhorn iterations is attached to the four looped layers. The native Qwen3.5 vision tower keeps patch 16, temporal patch 2, spatial merge 2 and the 2,304-position buffer. The source stores its backbone in FP32 and its MTP draft in BF16 as a separate mtp.safetensors component; both precision contracts are preserved.
106
+
107
+ *This repo is generated by GPT-6.1-Sol.*
chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- set image_count = namespace(value=0) %}
2
+ {%- set video_count = namespace(value=0) %}
3
+ {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
+ {%- if content is string %}
5
+ {{- content }}
6
+ {%- elif content is iterable and content is not mapping %}
7
+ {%- for item in content %}
8
+ {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
+ {%- if is_system_content %}
10
+ {{- raise_exception('System message cannot contain images.') }}
11
+ {%- endif %}
12
+ {%- if do_vision_count %}
13
+ {%- set image_count.value = image_count.value + 1 %}
14
+ {%- endif %}
15
+ {%- if add_vision_id %}
16
+ {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
+ {%- endif %}
18
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
+ {%- elif 'video' in item or item.type == 'video' %}
20
+ {%- if is_system_content %}
21
+ {{- raise_exception('System message cannot contain videos.') }}
22
+ {%- endif %}
23
+ {%- if do_vision_count %}
24
+ {%- set video_count.value = video_count.value + 1 %}
25
+ {%- endif %}
26
+ {%- if add_vision_id %}
27
+ {{- 'Video ' ~ video_count.value ~ ': ' }}
28
+ {%- endif %}
29
+ {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
+ {%- elif 'text' in item %}
31
+ {{- item.text }}
32
+ {%- else %}
33
+ {{- raise_exception('Unexpected item type in content.') }}
34
+ {%- endif %}
35
+ {%- endfor %}
36
+ {%- elif content is none or content is undefined %}
37
+ {{- '' }}
38
+ {%- else %}
39
+ {{- raise_exception('Unexpected content type.') }}
40
+ {%- endif %}
41
+ {%- endmacro %}
42
+ {%- if not messages %}
43
+ {{- raise_exception('No messages provided.') }}
44
+ {%- endif %}
45
+ {%- if tools and tools is iterable and tools is not mapping %}
46
+ {{- '<|im_start|>system\n' }}
47
+ {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
+ {%- for tool in tools %}
49
+ {{- "\n" }}
50
+ {{- tool | tojson }}
51
+ {%- endfor %}
52
+ {{- "\n</tools>" }}
53
+ {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
+ {%- if messages[0].role == 'system' %}
55
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
56
+ {%- if content %}
57
+ {{- '\n\n' + content }}
58
+ {%- endif %}
59
+ {%- endif %}
60
+ {{- '<|im_end|>\n' }}
61
+ {%- else %}
62
+ {%- if messages[0].role == 'system' %}
63
+ {%- set content = render_content(messages[0].content, false, true)|trim %}
64
+ {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
+ {%- endif %}
66
+ {%- endif %}
67
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
+ {%- for message in messages[::-1] %}
69
+ {%- set index = (messages|length - 1) - loop.index0 %}
70
+ {%- if ns.multi_step_tool and message.role == "user" %}
71
+ {%- set content = render_content(message.content, false)|trim %}
72
+ {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
+ {%- set ns.multi_step_tool = false %}
74
+ {%- set ns.last_query_index = index %}
75
+ {%- endif %}
76
+ {%- endif %}
77
+ {%- endfor %}
78
+ {%- if ns.multi_step_tool %}
79
+ {{- raise_exception('No user query found in messages.') }}
80
+ {%- endif %}
81
+ {%- for message in messages %}
82
+ {%- set content = render_content(message.content, true)|trim %}
83
+ {%- if message.role == "system" %}
84
+ {%- if not loop.first %}
85
+ {{- raise_exception('System message must be at the beginning.') }}
86
+ {%- endif %}
87
+ {%- elif message.role == "user" %}
88
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
+ {%- elif message.role == "assistant" %}
90
+ {%- set reasoning_content = '' %}
91
+ {%- if message.reasoning_content is string %}
92
+ {%- set reasoning_content = message.reasoning_content %}
93
+ {%- else %}
94
+ {%- if '</think>' in content %}
95
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
+ {%- endif %}
98
+ {%- endif %}
99
+ {%- set reasoning_content = reasoning_content|trim %}
100
+ {%- if loop.index0 > ns.last_query_index %}
101
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
+ {%- else %}
103
+ {{- '<|im_start|>' + message.role + '\n' + content }}
104
+ {%- endif %}
105
+ {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
+ {%- for tool_call in message.tool_calls %}
107
+ {%- if tool_call.function is defined %}
108
+ {%- set tool_call = tool_call.function %}
109
+ {%- endif %}
110
+ {%- if loop.first %}
111
+ {%- if content|trim %}
112
+ {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
+ {%- else %}
114
+ {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
+ {%- endif %}
116
+ {%- else %}
117
+ {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
+ {%- endif %}
119
+ {%- if tool_call.arguments is defined %}
120
+ {%- for args_name, args_value in tool_call.arguments|items %}
121
+ {{- '<parameter=' + args_name + '>\n' }}
122
+ {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
+ {{- args_value }}
124
+ {{- '\n</parameter>\n' }}
125
+ {%- endfor %}
126
+ {%- endif %}
127
+ {{- '</function>\n</tool_call>' }}
128
+ {%- endfor %}
129
+ {%- endif %}
130
+ {{- '<|im_end|>\n' }}
131
+ {%- elif message.role == "tool" %}
132
+ {%- if loop.previtem and loop.previtem.role != "tool" %}
133
+ {{- '<|im_start|>user' }}
134
+ {%- endif %}
135
+ {{- '\n<tool_response>\n' }}
136
+ {{- content }}
137
+ {{- '\n</tool_response>' }}
138
+ {%- if not loop.last and loop.nextitem.role != "tool" %}
139
+ {{- '<|im_end|>\n' }}
140
+ {%- elif loop.last %}
141
+ {{- '<|im_end|>\n' }}
142
+ {%- endif %}
143
+ {%- else %}
144
+ {{- raise_exception('Unexpected message role.') }}
145
+ {%- endif %}
146
+ {%- endfor %}
147
+ {%- if add_generation_prompt %}
148
+ {{- '<|im_start|>assistant\n' }}
149
+ {%- if enable_thinking is defined and enable_thinking is false %}
150
+ {{- '<think>\n\n</think>\n\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "SenseNovaLoopForConditionalGeneration"
4
+ ],
5
+ "attn_implementation": "sdpa",
6
+ "bos_token_id": null,
7
+ "dtype": "bfloat16",
8
+ "eos_token_id": 248046,
9
+ "hidden_size": 8,
10
+ "image_token_id": 248056,
11
+ "loop_settings": {
12
+ "counts": [
13
+ 2
14
+ ],
15
+ "end": 8,
16
+ "eval_count": 2,
17
+ "start": 4
18
+ },
19
+ "model_type": "sensenova_loop",
20
+ "pad_token_id": 248044,
21
+ "patch_conv3d": true,
22
+ "text_config": {
23
+ "attention_bias": false,
24
+ "attention_dropout": 0.0,
25
+ "attn_output_gate": true,
26
+ "bos_token_id": null,
27
+ "dtype": "bfloat16",
28
+ "eos_token_id": 248044,
29
+ "full_attention_interval": 4,
30
+ "head_dim": 32,
31
+ "hidden_act": "silu",
32
+ "hidden_size": 8,
33
+ "initializer_range": 0.02,
34
+ "intermediate_size": 32,
35
+ "layer_types": [
36
+ "linear_attention",
37
+ "linear_attention",
38
+ "linear_attention",
39
+ "full_attention",
40
+ "linear_attention",
41
+ "linear_attention",
42
+ "linear_attention",
43
+ "full_attention",
44
+ "linear_attention",
45
+ "linear_attention",
46
+ "linear_attention",
47
+ "full_attention"
48
+ ],
49
+ "linear_conv_kernel_dim": 4,
50
+ "linear_key_head_dim": 32,
51
+ "linear_num_key_heads": 4,
52
+ "linear_num_value_heads": 8,
53
+ "linear_value_head_dim": 32,
54
+ "mamba_ssm_dtype": "float32",
55
+ "max_position_embeddings": 262144,
56
+ "mlp_only_layers": [],
57
+ "model_type": "qwen3_5_text",
58
+ "mtp_num_hidden_layers": 1,
59
+ "mtp_use_dedicated_embeddings": false,
60
+ "num_attention_heads": 8,
61
+ "num_hidden_layers": 12,
62
+ "num_key_value_heads": 4,
63
+ "pad_token_id": 248044,
64
+ "partial_rotary_factor": 0.25,
65
+ "rms_norm_eps": 1e-06,
66
+ "rope_parameters": {
67
+ "mrope_interleaved": true,
68
+ "mrope_section": [
69
+ 1,
70
+ 1,
71
+ 2
72
+ ],
73
+ "partial_rotary_factor": 0.25,
74
+ "rope_theta": 10000000,
75
+ "rope_type": "default"
76
+ },
77
+ "tie_word_embeddings": false,
78
+ "use_cache": true,
79
+ "vocab_size": 248320
80
+ },
81
+ "tie_word_embeddings": false,
82
+ "use_cache": true,
83
+ "video_token_id": 248057,
84
+ "vision_config": {
85
+ "deepstack_visual_indexes": [],
86
+ "depth": 4,
87
+ "dtype": "bfloat16",
88
+ "hidden_act": "gelu_pytorch_tanh",
89
+ "hidden_size": 64,
90
+ "in_channels": 3,
91
+ "initializer_range": 0.02,
92
+ "intermediate_size": 128,
93
+ "model_type": "qwen3_5_vision",
94
+ "num_heads": 2,
95
+ "num_position_embeddings": 2304,
96
+ "out_hidden_size": 8,
97
+ "patch_size": 16,
98
+ "spatial_merge_size": 2,
99
+ "temporal_patch_size": 2
100
+ },
101
+ "vision_end_token_id": 248054,
102
+ "vision_start_token_id": 248053,
103
+ "mhc": {
104
+ "end": 8,
105
+ "fixed_identity": false,
106
+ "iterations": 20,
107
+ "pre_scale": 1.0,
108
+ "start": 4,
109
+ "streams": 2
110
+ },
111
+ "model_name": "SenseNova-Loop-9B-Preview",
112
+ "auto_map": {
113
+ "AutoConfig": "modeling_sensenova_loop.SenseNovaLoopConfig",
114
+ "AutoModel": "modeling_sensenova_loop.SenseNovaLoopModel",
115
+ "AutoModelForCausalLM": "modeling_sensenova_loop.SenseNovaLoopForConditionalGeneration",
116
+ "AutoModelForImageTextToText": "modeling_sensenova_loop.SenseNovaLoopForConditionalGeneration"
117
+ }
118
+ }
generation_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "eos_token_id": [
4
+ 248044,
5
+ 248046
6
+ ],
7
+ "transformers_version": "5.18.0",
8
+ "use_cache": true,
9
+ "temperature": 1.0,
10
+ "do_sample": true,
11
+ "top_p": 0.95
12
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:063ba8b84b81edad395358bb6a51ff8e4e612a140bc62a5347b28d2aaccb845e
3
+ size 18226832
modeling_sensenova_loop.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face remote-code support for shared-weight SenseNova Loop and MHC.
2
+
3
+ This single file contains configuration, plan validation, MHC routing and the
4
+ model implementation. The original physical layer weights are reused on every
5
+ Loop visit; autoregressive cache states are independent per visit.
6
+ """
7
+ import copy
8
+ import math
9
+ from contextvars import ContextVar
10
+
11
+ import torch
12
+ from torch import nn
13
+ from transformers import Qwen3_5Config
14
+ from transformers.cache_utils import DynamicCache
15
+ from transformers.models.qwen3_5 import modeling_qwen3_5 as hf
16
+
17
+
18
+ _attention_capture = ContextVar("sensenova_attention_capture", default=None)
19
+
20
+
21
+ class SenseNovaLoopConfig(Qwen3_5Config):
22
+ model_type = "sensenova_loop"
23
+
24
+
25
+ def execution_plan(config):
26
+ """Return physical layer indices in the order they execute during inference."""
27
+ loop = config.get("loop_settings")
28
+ if not isinstance(loop, dict):
29
+ raise ValueError("This implementation requires explicit loop_settings")
30
+
31
+ text_config = config["text_config"]
32
+ num_layers = text_config["num_hidden_layers"]
33
+ start, end, count = loop.get("start"), loop.get("end"), loop.get("eval_count")
34
+ if any(type(value) is not int for value in (num_layers, start, end, count)) or not (
35
+ 0 <= start < end <= num_layers and count >= 1
36
+ ):
37
+ raise ValueError("Invalid Loop interval or eval_count")
38
+ if loop.get("counts") != [count]:
39
+ raise ValueError("Only fixed-count Loop checkpoints are supported")
40
+
41
+ layer_types = text_config["layer_types"]
42
+ if len(layer_types) != num_layers or set(layer_types) - {"full_attention", "linear_attention"}:
43
+ raise ValueError("Unexpected physical layer types")
44
+ if "full_attention" not in layer_types:
45
+ raise ValueError("At least one full-attention layer is required for sequence-length tracking")
46
+
47
+ mhc = config.get("mhc")
48
+ if mhc is not None:
49
+ mhc_start, mhc_end, streams, iterations = (
50
+ mhc.get(key) for key in ("start", "end", "streams", "iterations")
51
+ )
52
+ if (
53
+ any(type(value) is not int for value in (mhc_start, mhc_end, streams, iterations))
54
+ or not 0 <= mhc_start < mhc_end <= num_layers
55
+ or streams not in (2, 4)
56
+ or iterations < 1
57
+ ):
58
+ raise ValueError("Invalid MHC range, stream count, or iterations")
59
+ if type(mhc.get("fixed_identity")) is not bool:
60
+ raise ValueError("MHC fixed_identity must be a boolean")
61
+ scale = mhc.get("pre_scale", 1.0)
62
+ if not isinstance(scale, (int, float)) or not math.isfinite(scale) or scale <= 0:
63
+ raise ValueError("Invalid MHC pre_scale")
64
+ allowed = {"start", "end", "streams", "iterations", "fixed_identity", "pre_scale"}
65
+ if set(mhc) - allowed:
66
+ raise ValueError("Unknown MHC configuration fields")
67
+ phase_boundaries = (0, start, end, num_layers)
68
+ if not any(
69
+ left <= mhc_start < mhc_end <= right
70
+ for left, right in zip(phase_boundaries, phase_boundaries[1:])
71
+ ):
72
+ raise ValueError("MHC cannot cross a Loop phase boundary")
73
+
74
+ return tuple(range(start)) + tuple(range(start, end)) * count + tuple(range(end, num_layers))
75
+
76
+
77
+ def sinkhorn(logits, iterations):
78
+ """Normalize routing logits across rows and columns in log space."""
79
+ logits = logits.float()
80
+ for _ in range(iterations):
81
+ logits = logits - torch.logsumexp(logits, dim=-1, keepdim=True)
82
+ logits = logits - torch.logsumexp(logits, dim=-2, keepdim=True)
83
+ return logits.exp()
84
+
85
+
86
+ class MHCRouter(nn.Module):
87
+ def __init__(self, dim, settings):
88
+ super().__init__()
89
+ self.streams = settings["streams"]
90
+ self.iterations = settings["iterations"]
91
+ self.fixed_identity = settings["fixed_identity"]
92
+ self.pre_scale = settings.get("pre_scale", 1.0)
93
+ streams = self.streams
94
+ output_size = 2 * streams + (0 if self.fixed_identity else streams * streams)
95
+ self.weight = nn.Parameter(torch.zeros(output_size, streams * dim, dtype=torch.float32))
96
+ self.bias = nn.Parameter(torch.zeros(output_size, dtype=torch.float32))
97
+ with torch.no_grad():
98
+ initial_probabilities = {2: (0.25, 0.75), 4: (0.0625, 0.1875, 0.25, 0.5)}[streams]
99
+ self.bias[:streams].copy_(torch.logit(torch.tensor(initial_probabilities, device=self.bias.device)))
100
+ if not self.fixed_identity:
101
+ diagonal = math.log((1 - (streams - 1) / 16) * 16)
102
+ self.bias[2 * streams:].copy_((torch.eye(streams, device=self.bias.device) * diagonal).flatten())
103
+
104
+ def _apply(self, fn, recurse=True):
105
+ # Keep the original FP32 values when the surrounding model is cast to BF16.
106
+ def keep_fp32(tensor):
107
+ result = fn(tensor)
108
+ if tensor.is_floating_point() and result.dtype != torch.float32:
109
+ if tensor.device.type == "meta" and result.device.type != "meta":
110
+ return torch.empty_like(result, dtype=torch.float32)
111
+ return tensor.to(device=result.device, dtype=torch.float32)
112
+ return result
113
+
114
+ return super()._apply(keep_fp32, recurse=recurse)
115
+
116
+ def forward(self, residual):
117
+ if self.weight.dtype != torch.float32 or self.bias.dtype != torch.float32:
118
+ raise RuntimeError("mHC routers require FP32 parameters; configure FSDP per-module precision")
119
+ with torch.autocast(device_type=residual.device.type, enabled=False):
120
+ flat = residual.flatten(-2).float()
121
+ normalized = flat * torch.rsqrt(flat.square().mean(-1, keepdim=True) + 1e-6)
122
+ logits = torch.nn.functional.linear(normalized, self.weight, self.bias)
123
+ streams = self.streams
124
+ pre = logits[..., :streams].sigmoid()
125
+ post = 2 * logits[..., streams:2 * streams].sigmoid()
126
+ if self.fixed_identity:
127
+ mix = torch.eye(streams, device=flat.device, dtype=torch.float32).expand(
128
+ *flat.shape[:-1], streams, streams
129
+ )
130
+ else:
131
+ mix = sinkhorn(logits[..., 2 * streams:].unflatten(-1, (streams, streams)), self.iterations)
132
+ branch = torch.matmul(pre.to(residual.dtype).unsqueeze(-2), residual).squeeze(-2)
133
+ if self.pre_scale != 1.0:
134
+ branch = branch * self.pre_scale
135
+ return branch, post, mix
136
+
137
+ @staticmethod
138
+ def merge(residual, branch, post, mix):
139
+ """Mix residual streams and add the branch output to each stream."""
140
+ with torch.autocast(device_type=residual.device.type, enabled=False):
141
+ mixed_residual = torch.matmul(mix.to(residual.dtype), residual)
142
+ branch_update = post.to(branch.dtype).unsqueeze(-1) * branch.unsqueeze(-2)
143
+ return mixed_residual + branch_update
144
+
145
+
146
+ def enter(hidden, physical, settings):
147
+ """Expand to residual streams at the first mHC layer, or restore their shape."""
148
+ if physical == settings["start"]:
149
+ return hidden.unsqueeze(-2).expand(
150
+ *hidden.shape[:-1], settings["streams"], hidden.shape[-1]
151
+ ).contiguous()
152
+ return hidden.unflatten(-1, (settings["streams"], -1))
153
+
154
+
155
+ def leave(residual, physical, settings):
156
+ """Reduce streams at the last mHC layer; otherwise carry them forward."""
157
+ return residual.mean(-2) if physical == settings["end"] - 1 else residual.flatten(-2)
158
+
159
+
160
+ class _CacheLayersView:
161
+ """Expose the execution slot when a layer accesses its physical index."""
162
+ def __init__(self, cache, physical, virtual):
163
+ self.cache = cache
164
+ self.physical = physical
165
+ self.virtual = virtual
166
+
167
+ def __getitem__(self, index):
168
+ if index != self.physical:
169
+ raise IndexError("A physical layer may only access its own virtual cache slot")
170
+ return self.cache.layers[self.virtual]
171
+
172
+
173
+ class _VisitCache:
174
+ """Map a physical attention module's layer_idx to one execution slot.
175
+
176
+ Mapping is local to this forward call: shared modules and their layer_idx are
177
+ never mutated. `layers` supports Transformers 5.12; state accessors support
178
+ newer official backbone cache APIs as well.
179
+ """
180
+ def __init__(self, cache, physical, virtual):
181
+ self.cache = cache
182
+ self.virtual = virtual
183
+ self.layers = _CacheLayersView(cache, physical, virtual)
184
+
185
+ def update(self, key_states, value_states, layer_idx, **kwargs):
186
+ return self.cache.update(key_states, value_states, self.virtual, **kwargs)
187
+
188
+ def has_previous_state(self, layer_idx=None, **kwargs):
189
+ return self.cache.has_previous_state(self.virtual, **kwargs)
190
+
191
+ def update_conv_state(self, state, layer_idx, **kwargs):
192
+ return self.cache.update_conv_state(state, self.virtual, **kwargs)
193
+
194
+ def update_recurrent_state(self, state, layer_idx, **kwargs):
195
+ return self.cache.update_recurrent_state(state, self.virtual, **kwargs)
196
+
197
+ def get_conv_state(self, layer_idx, **kwargs):
198
+ return self.cache.get_conv_state(self.virtual, **kwargs)
199
+
200
+ def get_recurrent_state(self, layer_idx, **kwargs):
201
+ return self.cache.get_recurrent_state(self.virtual, **kwargs)
202
+
203
+ def is_recording_past(self, layer_idx):
204
+ return self.cache.is_recording_past(self.virtual)
205
+
206
+
207
+ class SenseNovaLoopTextModel(hf.Qwen3_5TextModel):
208
+ def forward(self, input_ids=None, attention_mask=None, position_ids=None,
209
+ past_key_values=None, inputs_embeds=None, use_cache=None,
210
+ output_hidden_states=None, output_attentions=None, **kwargs):
211
+ """Run Loop visits, optionally returning their states and attention weights.
212
+
213
+ Hidden states include the initial embedding and one state per visit.
214
+ MHC states between its boundaries retain their flattened residual streams.
215
+ Attention entries follow visit order, with None for linear-attention layers.
216
+ """
217
+ if (input_ids is None) == (inputs_embeds is None):
218
+ raise ValueError("Specify exactly one of input_ids or inputs_embeds")
219
+ if inputs_embeds is None:
220
+ inputs_embeds = self.embed_tokens(input_ids)
221
+ use_cache = self.config.use_cache if use_cache is None else use_cache
222
+ if output_hidden_states is None:
223
+ output_hidden_states = getattr(self.config, "output_hidden_states", False)
224
+ if output_attentions is None:
225
+ output_attentions = getattr(self.config, "output_attentions", False)
226
+ if not isinstance(output_hidden_states, bool):
227
+ raise TypeError("output_hidden_states must be a boolean.")
228
+ if output_attentions and self.config._attn_implementation != "eager":
229
+ raise ValueError(
230
+ "Attention weights require attn_implementation='eager' when loading the model."
231
+ )
232
+ if self.gradient_checkpointing and self.training and use_cache:
233
+ raise ValueError("Disable use_cache when training with gradient checkpointing")
234
+ if not use_cache and past_key_values is not None:
235
+ raise ValueError("past_key_values requires use_cache=True")
236
+ if past_key_values is not None and not isinstance(past_key_values, DynamicCache):
237
+ raise TypeError("Loop generation currently supports DynamicCache only")
238
+ if use_cache:
239
+ if past_key_values is None:
240
+ past_key_values = DynamicCache(config=self._loop_cache_config)
241
+ past_key_values._loop_execution_plan = self._loop_plan
242
+ elif getattr(past_key_values, "_loop_execution_plan", None) != self._loop_plan:
243
+ # GenerationMixin may allocate an empty physical-depth cache.
244
+ # A populated foreign cache cannot be reinterpreted safely.
245
+ if past_key_values.get_seq_length() != 0 or past_key_values.has_previous_state():
246
+ raise ValueError("A populated cache from another execution plan cannot be reused")
247
+ past_key_values = DynamicCache(config=self._loop_cache_config)
248
+ past_key_values._loop_execution_plan = self._loop_plan
249
+
250
+ if position_ids is None:
251
+ seen = past_key_values.get_seq_length() if past_key_values is not None else 0
252
+ position_ids = torch.arange(inputs_embeds.shape[1], device=inputs_embeds.device) + seen
253
+ position_ids = position_ids.view(1, 1, -1).expand(4, inputs_embeds.shape[0], -1)
254
+ elif position_ids.ndim == 2:
255
+ position_ids = position_ids.unsqueeze(0).expand(4, -1, -1)
256
+ if position_ids.ndim == 3 and position_ids.shape[0] == 4:
257
+ text_positions, position_ids = position_ids[0], position_ids[1:]
258
+ else:
259
+ text_positions = None
260
+
261
+ if isinstance(attention_mask, dict):
262
+ masks = attention_mask
263
+ else:
264
+ mask_args = dict(
265
+ config=self._loop_cache_config,
266
+ inputs_embeds=inputs_embeds,
267
+ attention_mask=attention_mask,
268
+ past_key_values=past_key_values,
269
+ position_ids=text_positions,
270
+ )
271
+ if hasattr(hf, "create_recurrent_attention_mask"):
272
+ linear_mask = hf.create_recurrent_attention_mask(**mask_args)
273
+ else:
274
+ linear_mask = self._update_linear_attn_mask(attention_mask, past_key_values)
275
+ masks = {
276
+ "full_attention": hf.create_causal_mask(**mask_args),
277
+ "linear_attention": linear_mask,
278
+ }
279
+
280
+ hidden = inputs_embeds
281
+ position_embeddings = self.rotary_emb(hidden, position_ids)
282
+ hidden_states = [hidden] if output_hidden_states else None
283
+ attentions = [] if output_attentions else None
284
+ hooks = []
285
+ capture_token = None
286
+
287
+ def record_attention(_module, _inputs, output):
288
+ if _attention_capture.get() is attentions:
289
+ if output[1] is None:
290
+ raise RuntimeError("The attention backend did not return attention weights.")
291
+ attentions.append(output[1])
292
+
293
+ # Repeated visits share weights, but each visit has its own cache slot.
294
+ try:
295
+ if attentions is not None:
296
+ for layer in self.layers:
297
+ if hasattr(layer, "self_attn"):
298
+ hooks.append(layer.self_attn.register_forward_hook(record_attention))
299
+ capture_token = _attention_capture.set(attentions)
300
+ for execution_index, physical_index in enumerate(self._loop_plan):
301
+ layer_type = self.config.layer_types[physical_index]
302
+ if attentions is not None and layer_type == "linear_attention":
303
+ attentions.append(None)
304
+ cache = (
305
+ _VisitCache(past_key_values, physical_index, execution_index)
306
+ if use_cache else None
307
+ )
308
+ hidden = self.layers[physical_index](
309
+ hidden, position_embeddings=position_embeddings,
310
+ attention_mask=masks[layer_type],
311
+ position_ids=text_positions, past_key_values=cache,
312
+ use_cache=use_cache, **kwargs,
313
+ )
314
+ if hidden_states is not None:
315
+ hidden_states.append(hidden)
316
+ finally:
317
+ if capture_token is not None:
318
+ _attention_capture.reset(capture_token)
319
+ for hook in hooks:
320
+ hook.remove()
321
+
322
+ hidden = self.norm(hidden)
323
+ if hidden_states is not None:
324
+ hidden_states[-1] = hidden
325
+ return hf.Qwen3_5ModelOutputWithPast(
326
+ last_hidden_state=hidden, past_key_values=past_key_values,
327
+ hidden_states=tuple(hidden_states) if hidden_states is not None else None,
328
+ attentions=tuple(attentions) if attentions is not None else None,
329
+ )
330
+
331
+
332
+ class MHCDecoderLayer(hf.Qwen3_5DecoderLayer):
333
+ def forward(self, hidden_states, position_embeddings, attention_mask=None,
334
+ position_ids=None, past_key_values=None, **kwargs):
335
+ residual = enter(hidden_states, self._mhc_physical, self._mhc_settings)
336
+ branch, post, mix = self.mhc_attn(residual)
337
+ branch = self.input_layernorm(branch)
338
+ kind = self.layer_type if hasattr(self, "layer_type") else self.block_type
339
+ if kind == "linear_attention":
340
+ branch = self.linear_attn(hidden_states=branch, cache_params=past_key_values,
341
+ attention_mask=attention_mask, **kwargs)
342
+ else:
343
+ branch, _ = self.self_attn(
344
+ hidden_states=branch, position_embeddings=position_embeddings,
345
+ attention_mask=attention_mask, position_ids=position_ids,
346
+ past_key_values=past_key_values, **kwargs)
347
+ residual = MHCRouter.merge(residual, branch, post, mix)
348
+ branch, post, mix = self.mhc_ffn(residual)
349
+ branch = self.mlp(self.post_attention_layernorm(branch))
350
+ return leave(MHCRouter.merge(residual, branch, post, mix),
351
+ self._mhc_physical, self._mhc_settings)
352
+
353
+
354
+ def _configure_text_model(text, config):
355
+ raw = config.to_dict()
356
+ plan = execution_plan(raw)
357
+ # Change only these new model instances, preserving canonical state_dict
358
+ # names and avoiding a second allocation of the full backbone.
359
+ text.__class__ = SenseNovaLoopTextModel
360
+ text._loop_plan = plan
361
+ text._loop_cache_config = copy.deepcopy(text.config)
362
+ text._loop_cache_config.num_hidden_layers = len(plan)
363
+ text._loop_cache_config.layer_types = [text.config.layer_types[i] for i in plan]
364
+ if raw.get("mhc") is not None:
365
+ settings = raw["mhc"]
366
+ for i in range(settings["start"], settings["end"]):
367
+ layer = text.layers[i]
368
+ layer.__class__ = MHCDecoderLayer
369
+ layer._mhc_physical, layer._mhc_settings = i, dict(settings)
370
+ layer.mhc_attn = MHCRouter(text.config.hidden_size, settings)
371
+ layer.mhc_ffn = MHCRouter(text.config.hidden_size, settings)
372
+
373
+
374
+ class SenseNovaLoopModel(hf.Qwen3_5Model):
375
+ config_class = SenseNovaLoopConfig
376
+ _keep_in_fp32_modules = ["mhc_attn", "mhc_ffn"]
377
+ _no_split_modules = ["Qwen3_5DecoderLayer", "MHCDecoderLayer", "Qwen3_5VisionBlock"]
378
+ _keys_to_ignore_on_load_unexpected = [r"^mtp\."]
379
+
380
+ def __init__(self, config):
381
+ execution_plan(config.to_dict())
382
+ super().__init__(config)
383
+ _configure_text_model(self.language_model, config)
384
+
385
+
386
+ class SenseNovaLoopForConditionalGeneration(hf.Qwen3_5ForConditionalGeneration):
387
+ config_class = SenseNovaLoopConfig
388
+ _keep_in_fp32_modules = ["mhc_attn", "mhc_ffn"]
389
+ _no_split_modules = ["Qwen3_5DecoderLayer", "MHCDecoderLayer", "Qwen3_5VisionBlock"]
390
+ _keys_to_ignore_on_load_unexpected = [r"^mtp\."]
391
+
392
+ def __init__(self, config):
393
+ execution_plan(config.to_dict())
394
+ super().__init__(config)
395
+ _configure_text_model(self.model.language_model, config)
mtp.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8ba6e24e21805901db005ca4e21c3e9e5364550439a3883f797cf183332d5ed
3
+ size 19824
preprocessor_config.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_convert_rgb": true,
3
+ "do_normalize": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.5,
8
+ 0.5,
9
+ 0.5
10
+ ],
11
+ "image_processor_type": "Qwen2VLImageProcessor",
12
+ "image_std": [
13
+ 0.5,
14
+ 0.5,
15
+ 0.5
16
+ ],
17
+ "merge_size": 2,
18
+ "patch_size": 16,
19
+ "resample": 3,
20
+ "rescale_factor": 0.00392156862745098,
21
+ "size": {
22
+ "longest_edge": 16777216,
23
+ "shortest_edge": 65536
24
+ },
25
+ "temporal_patch_size": 2
26
+ }
processor_config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor": {
3
+ "do_convert_rgb": true,
4
+ "do_normalize": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_processor_type": "Qwen2VLImageProcessor",
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "merge_size": 2,
19
+ "patch_size": 16,
20
+ "resample": 3,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": {
23
+ "longest_edge": 16777216,
24
+ "shortest_edge": 65536
25
+ },
26
+ "temporal_patch_size": 2
27
+ },
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "video_processor": {
30
+ "do_convert_rgb": true,
31
+ "do_normalize": true,
32
+ "do_rescale": true,
33
+ "do_resize": true,
34
+ "do_sample_frames": true,
35
+ "fps": 2,
36
+ "image_mean": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_std": [
42
+ 0.5,
43
+ 0.5,
44
+ 0.5
45
+ ],
46
+ "max_frames": 768,
47
+ "merge_size": 2,
48
+ "min_frames": 4,
49
+ "patch_size": 16,
50
+ "resample": 3,
51
+ "rescale_factor": 0.00392156862745098,
52
+ "return_metadata": false,
53
+ "size": {
54
+ "longest_edge": 25165824,
55
+ "shortest_edge": 4096
56
+ },
57
+ "temporal_patch_size": 2,
58
+ "video_processor_type": "Qwen3VLVideoProcessor"
59
+ }
60
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
+ size 19989325
tokenizer_config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|im_end|>",
10
+ "errors": "replace",
11
+ "image_token": "<|image_pad|>",
12
+ "is_local": true,
13
+ "local_files_only": false,
14
+ "model_max_length": 262144,
15
+ "model_specific_special_tokens": {
16
+ "audio_bos_token": "<|audio_start|>",
17
+ "audio_eos_token": "<|audio_end|>",
18
+ "audio_token": "<|audio_pad|>",
19
+ "image_token": "<|image_pad|>",
20
+ "video_token": "<|video_pad|>",
21
+ "vision_bos_token": "<|vision_start|>",
22
+ "vision_eos_token": "<|vision_end|>"
23
+ },
24
+ "pad_token": "<|endoftext|>",
25
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
26
+ "processor_class": "Qwen3VLProcessor",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "Qwen2Tokenizer",
29
+ "unk_token": null,
30
+ "video_token": "<|video_pad|>",
31
+ "vision_bos_token": "<|vision_start|>",
32
+ "vision_eos_token": "<|vision_end|>"
33
+ }
video_preprocessor_config.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_convert_rgb": true,
3
+ "do_normalize": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "do_sample_frames": true,
7
+ "fps": 2,
8
+ "image_mean": [
9
+ 0.5,
10
+ 0.5,
11
+ 0.5
12
+ ],
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "max_frames": 768,
19
+ "merge_size": 2,
20
+ "min_frames": 4,
21
+ "patch_size": 16,
22
+ "resample": 3,
23
+ "rescale_factor": 0.00392156862745098,
24
+ "return_metadata": false,
25
+ "size": {
26
+ "longest_edge": 25165824,
27
+ "shortest_edge": 4096
28
+ },
29
+ "temporal_patch_size": 2,
30
+ "video_processor_type": "Qwen3VLVideoProcessor"
31
+ }