Add a 4/16/4 recurrent 1B architecture and 100-step trial results

#1
README.md CHANGED
@@ -2,34 +2,62 @@
2
  license: apache-2.0
3
  library_name: transformers
4
  tags:
5
- - nano_dense
6
  - architecture-only
7
  - from-scratch
8
  - deepseek-v4
9
  - dense
 
10
  language:
11
  - en
12
  - zh
13
  ---
14
 
15
- # Nano-1B
16
 
17
- A **1,076,700,672-parameter dense decoder architecture for training from scratch**.
18
- This repository contains configuration, model code and a DeepSeek V4 tokenizer.
19
- **There are no model weights or trained checkpoints.** Initialize with
20
- `AutoModelForCausalLM.from_config`, rather than loading pretrained model weights.
21
 
22
- The dimensions follow [MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B).
23
- Normalization, dense SwiGLU and interleaved RoPE reuse Transformers' DeepSeek V4
24
- components. `NanoDenseForCausalLM` supplies the model entry point and standard
25
- global grouped-query attention. The tokenizer is pinned to
26
- [DeepSeek-V4-Flash-Base, revision 8855555](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-Base/tree/8855555deef230a27a21a8d6f294b7b7497759b6).
27
 
28
  ## Architecture
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  | Setting | Value |
31
  | --- | ---: |
32
- | Decoder layers | 24 |
 
 
 
33
  | Residual hidden size | 1,536 |
34
  | Dense SwiGLU intermediate size | 4,608 |
35
  | Query / key-value heads | 16 / 2 |
@@ -37,7 +65,7 @@ global grouped-query attention. The tokenizer is pinned to
37
  | Attention projection width | 2,048 |
38
  | Attention pattern | Global causal GQA in every layer |
39
  | Normalization | DeepseekV4RMSNorm, epsilon 1e-6 |
40
- | Position encoding | V4 interleaved RoPE over all 128 head dimensions |
41
  | RoPE theta | 5,000,000 |
42
  | Configured context target | 131,072 tokens |
43
  | Vocabulary | 129,280 |
@@ -46,36 +74,28 @@ global grouped-query attention. The tokenizer is pinned to
46
  | Total parameters | **1,076,700,672** |
47
  | Excluding embedding and output head | **679,552,512** |
48
 
49
- Each layer computes:
50
-
51
- ```text
52
- h = x + GQA(RMSNorm(x))
53
- y = h + SwiGLU(RMSNorm(h))
54
- ```
55
-
56
- The attention width is independent of the residual width: Q projects
57
- 1536 -> 2048, K/V each project 1536 -> 256, and O projects 2048 -> 1536.
58
- All 24 layers have independent parameters. Linear and embedding weights use
59
- normal initialization with standard deviation 0.02; RMSNorm weights start at one.
60
  No depth-dependent initialization scaling is applied.
61
 
62
- 128K is a configuration target, not a measured capability of an untrained model.
63
- Long-context performance requires a suitable training curriculum and evaluation.
64
- At batch size one, a complete 128K BF16 KV cache occupies **3 GiB**, excluding
65
- weights, temporary tensors and allocator overhead.
66
 
67
  ## Initialize for training
68
 
69
- ```bash
70
- pip install -r requirements.txt
71
- ```
72
 
73
  ```python
74
  import torch
75
  from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
76
 
77
- repo = "bowang0911/Nano-1B"
78
  config = AutoConfig.from_pretrained(repo, trust_remote_code=True)
 
79
  tokenizer = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
80
  model = AutoModelForCausalLM.from_config(
81
  config,
@@ -83,70 +103,97 @@ model = AutoModelForCausalLM.from_config(
83
  attn_implementation="sdpa",
84
  dtype=torch.bfloat16,
85
  )
86
- # Randomly initialized model; pass it to your training loop / Trainer.
87
- ```
88
-
89
- To inspect the full architecture without allocating weight storage:
90
-
91
- ```python
92
- with torch.device("meta"):
93
- model = AutoModelForCausalLM.from_config(config, trust_remote_code=True)
94
- print(sum(p.numel() for p in model.parameters())) # 1076700672
95
  ```
96
 
97
- From a local copy, `python count_parameters.py --meta` checks the analytical
98
- count against a full-size meta-device model.
99
-
100
- ## Tokenized data and packing
 
101
 
102
- `tokenizer.json` is byte-identical to the pinned DeepSeek V4 artifact. The only
103
- tokenizer-config change is `model_max_length=131072`; token IDs and encoding are
104
- unchanged. `tokenizer_provenance.json` records upstream and published checksums.
105
- Existing data encoded with this pinned tokenizer can be reused directly.
106
- The pretraining convention used here is raw text with `add_special_tokens=False`
107
- and one EOS token (ID 1) appended per document. No chat template is provided.
108
 
109
- For ordinary padded batches, pass a two-dimensional `attention_mask` and set
110
- padding labels to `-100`. Default positions are derived from that mask, including
111
- when continuing a KV cache. The tokenizer uses EOS as its padding token; the
112
- model embedding has no dedicated zeroed padding row, preserving the EOS embedding.
113
 
114
- For padding-free document packing, use one flattened row (`batch_size=1`) and
115
- reset `position_ids` to zero at each document boundary. Omit `attention_mask`, or
116
- pass an all-ones mask. The model isolates documents and masks the first label of
117
- each subsequent document so the LM loss does not cross boundaries. Packing with
118
- padding, nonzero restart positions or multiple batch rows raises an error.
119
- KV caching is disabled during training and for packed sequences. Normal
120
- unpacked evaluation/generation supports dynamic KV caching.
121
-
122
- Eager and SDPA backends are validated on CPU. The FlashAttention adapter receives
123
- packed positions and dispatches separate document segments; running actual
124
- FlashAttention kernels requires a compatible GPU installation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  ## Validation
127
 
128
- Validated with Python 3.12.11, PyTorch 2.12.0 and Transformers 5.9.0:
 
129
 
130
- - Full-size meta parameter count, including the wider attention projections.
131
- - An independent complex-number RoPE / attention reference, including large positions.
132
- - Training loss, finite gradients through every parameter, an AdamW update,
133
- and gradient-checkpointing equivalence.
134
- - Eager/SDPA cached chunked decoding and cached/uncached greedy generation.
135
- - Left/right padding, packed-document isolation, boundary loss masking and
136
- FlashAttention varlen dispatch with external GPU kernels stubbed.
137
- - FP32/BF16 save/reload and invalid configuration override rejection.
138
- - Tokenizer parity with the pinned source.
139
 
140
- The 11 local behavioral tests are kept outside this model repository. This is
141
- small-model CPU validation, not full-size training, GPU throughput or 128K quality
142
- validation. Transformers is pinned because the implementation imports its V4
143
- components and attention/cache interfaces.
144
 
145
  ## Sources and licenses
146
 
147
- - [MiniCPM5-1B configuration](https://huggingface.co/openbmb/MiniCPM5-1B/blob/87179e5c1f455ef22e6223592d2d61351b525bfc/config.json): architecture dimensions.
148
  - [Transformers 5.9.0 DeepSeek V4](https://github.com/huggingface/transformers/blob/v5.9.0/src/transformers/models/deepseek_v4/modeling_deepseek_v4.py): RMSNorm, dense MLP and interleaved RoPE.
149
  - Model implementation: Apache-2.0, see `LICENSE`.
150
  - DeepSeek tokenizer assets: MIT, see `LICENSE-TOKENIZER`.
151
 
152
- No pretrained MiniCPM or DeepSeek model weights are included or converted.
 
2
  license: apache-2.0
3
  library_name: transformers
4
  tags:
5
+ - nano_dense_loop
6
  - architecture-only
7
  - from-scratch
8
  - deepseek-v4
9
  - dense
10
+ - recurrent
11
  language:
12
  - en
13
  - zh
14
  ---
15
 
16
+ # Recurrent Nano-1B
17
 
18
+ A **1,076,700,672-parameter dense decoder architecture for training from scratch**,
19
+ with a shared core that can run multiple times. This revision contains configuration,
20
+ model code and the same pinned DeepSeek V4 tokenizer. **No model weights or trained
21
+ checkpoints are included.** Initialize with `AutoModelForCausalLM.from_config`.
22
 
23
+ The base dimensions follow [MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B).
24
+ Normalization, dense SwiGLU and interleaved RoPE use Transformers' DeepSeek V4
25
+ components. `NanoDenseForCausalLM` supplies global grouped-query attention and
26
+ the recurrent execution schedule. The model type is `nano_dense_loop`.
 
27
 
28
  ## Architecture
29
 
30
+ ```text
31
+ DSV4 tokens → embedding → prelude (4 layers)
32
+ → core (16 layers) → same core (16 layers)
33
+ → coda (4 layers) → final RMSNorm → LM head
34
+ ```
35
+
36
+ | Component | Physical layers | Executions at K=2 |
37
+ | --- | ---: | ---: |
38
+ | Prelude | 4 | 4 |
39
+ | Shared core | 16 | 32 |
40
+ | Coda | 4 | 4 |
41
+ | Total | **24** | **40** |
42
+
43
+ The prelude runs once, the core runs `num_core_iterations` times, and the coda
44
+ runs once. Each core pass receives the previous pass's output and reuses the
45
+ same 16 sets of parameters. Token positions stay the same between passes.
46
+ Backpropagation traverses every pass. No extra boundary normalization, anchor
47
+ injection, adaptive halting or truncated backpropagation is added.
48
+
49
+ The default is **K=2**. K=1 is exactly the original 24-layer execution for the
50
+ same weights. Physical layer names and weight shapes are unchanged; changing
51
+ K does not add parameters. The split preserves the 1/6, 2/3, 1/6 proportions of
52
+ the earlier 36-layer Nano loop candidate; it is an initial candidate, not a
53
+ measured optimum or a complete reproduction of a paper's training recipe.
54
+
55
  | Setting | Value |
56
  | --- | ---: |
57
+ | Physical decoder layers | 24 |
58
+ | Prelude / core / coda | 4 / 16 / 4 |
59
+ | Default core iterations | 2 |
60
+ | Default effective depth | 40 |
61
  | Residual hidden size | 1,536 |
62
  | Dense SwiGLU intermediate size | 4,608 |
63
  | Query / key-value heads | 16 / 2 |
 
65
  | Attention projection width | 2,048 |
66
  | Attention pattern | Global causal GQA in every layer |
67
  | Normalization | DeepseekV4RMSNorm, epsilon 1e-6 |
68
+ | Position encoding | V4 interleaved full-head RoPE |
69
  | RoPE theta | 5,000,000 |
70
  | Configured context target | 131,072 tokens |
71
  | Vocabulary | 129,280 |
 
74
  | Total parameters | **1,076,700,672** |
75
  | Excluding embedding and output head | **679,552,512** |
76
 
77
+ Each layer computes `h = x + GQA(RMSNorm(x))`, then
78
+ `y = h + SwiGLU(RMSNorm(h))`. Q projects 1536 → 2048, K/V each project
79
+ 1536 → 256, and O projects 2048 → 1536. Linear and embedding weights use normal
80
+ initialization with standard deviation 0.02; RMSNorm weights start at one.
 
 
 
 
 
 
 
81
  No depth-dependent initialization scaling is applied.
82
 
83
+ Each execution has a separate KV cache slot, including the two executions of
84
+ each shared core layer. At batch size one, a complete 128K BF16 KV cache occupies
85
+ **5 GiB at K=2**, versus 3 GiB at K=1, excluding weights and temporary tensors.
86
+ The 128K context is a configuration target, not a validated capability.
87
 
88
  ## Initialize for training
89
 
90
+ Install `requirements.txt`, then run this from a local checkout of this revision:
 
 
91
 
92
  ```python
93
  import torch
94
  from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
95
 
96
+ repo = "."
97
  config = AutoConfig.from_pretrained(repo, trust_remote_code=True)
98
+ config.num_core_iterations = 2
99
  tokenizer = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
100
  model = AutoModelForCausalLM.from_config(
101
  config,
 
103
  attn_implementation="sdpa",
104
  dtype=torch.bfloat16,
105
  )
 
 
 
 
 
 
 
 
 
106
  ```
107
 
108
+ When loading directly from the Hub before merge, pass the PR revision to
109
+ `AutoConfig.from_pretrained` and `AutoTokenizer.from_pretrained`; `main` still
110
+ contains the vanilla architecture. `num_core_iterations` can also be passed to
111
+ `forward` or `generate` for a specific call. Use the same K across distributed
112
+ training ranks. This implementation does not sample K randomly.
113
 
114
+ `python count_parameters.py --meta` verifies the full physical parameter count
115
+ without allocating weight storage and reports the configured execution depth
116
+ and KV cache size. `--core-iterations 1` checks the single-pass configuration.
 
 
 
117
 
118
+ ## Data, packing and cache
 
 
 
119
 
120
+ The tokenizer remains pinned to
121
+ [DeepSeek-V4-Flash-Base, revision 8855555](https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-Base/tree/8855555deef230a27a21a8d6f294b7b7497759b6).
122
+ `tokenizer.json` is byte-identical to the original artifact. The existing
123
+ `model_max_length=131072` tokenizer-config setting and token IDs are unchanged.
124
+ `tokenizer_provenance.json` records the hashes. Pretraining uses raw text with
125
+ `add_special_tokens=False` and one appended EOS (ID 1), without a chat template.
126
+
127
+ For padded batches, pass a two-dimensional `attention_mask` and set padding
128
+ labels to `-100`. Default positions are derived from the mask. For padding-free
129
+ packing, use one flattened row and reset `position_ids` at document boundaries.
130
+ Omit the mask or pass an all-ones mask. The model isolates documents and excludes
131
+ cross-document next-token targets. Packed batches with padding or multiple rows
132
+ are rejected. Training and packed evaluation disable KV caching.
133
+
134
+ Unpacked evaluation/generation supports dynamic cache only. The returned cache
135
+ records its depth and layer schedule; these cannot change during continuation.
136
+ An empty vanilla `DynamicCache` is accepted and replaced with the loop cache.
137
+ Populated untagged caches and other cache implementations are rejected.
138
+
139
+ ## 100-step trial
140
+
141
+ The full-size trial ran on the same CoreWeave 4×GB200 node as the dense baseline.
142
+ Both used seed 20260925, the same DSV4-tokenized English web stream and token
143
+ order, 2048-token sequences, global batch 32, and **6,553,600 training tokens**.
144
+ AdamW used LR 3e-4 with 10 warmup steps, betas 0.9/0.95, weight decay 0.1, and
145
+ gradient clipping 1.0; parameters and optimizer states were FP32 with BF16 autocast.
146
+ The loop trained at fixed K=2. Every 20 steps, both evaluation depths used the
147
+ same fixed samples from six existing validation panels. Each panel contains
148
+ 32 windows of 2048 tokens; the overall panel overlaps the domain panels.
149
+
150
+ | Metric at step 100 | Base 1B (Vanilla) | Recurrent 1B (K=2) |
151
+ | --- | ---: | ---: |
152
+ | Physical / effective layers | 24 / 24 | 24 / 40 |
153
+ | Prelude / core / coda | — | 4 / 16 / 4 |
154
+ | Final training CE | 6.9774 | 7.1601 |
155
+ | Overall validation CE | **7.2836** | 7.4369 |
156
+ | Overall validation BPB | **2.2104** | 2.2568 |
157
+ | Mean seconds per step, steps 11–100 | 0.741 | 0.920 |
158
+ | Peak allocated GiB/GPU | 29.97 | 33.13 |
159
+
160
+ On the recurrent checkpoint, evaluating with K=1 gives CE **7.5839** and BPB
161
+ **2.3012**. A second core pass improves that checkpoint, but it does not outperform
162
+ the separately trained dense baseline in this trial. These runs match training
163
+ tokens, not compute. One seed and 100 steps do not establish long-run quality.
164
+ CE includes EOS; BPB uses text-token NLL and exact UTF-8 byte counts, excluding
165
+ special tokens from both numerator and denominator.
166
+
167
+ - [Dense baseline](https://wandb.ai/pplx-ai/nano-1b-ablation/runs/opurhf9m)
168
+ - [Recurrent trial](https://wandb.ai/pplx-ai/nano-1b-ablation/runs/h73o4lvs)
169
+
170
+ The trial completed with finite losses, gradients and weight-update probes in
171
+ all three sections. Model/optimizer weights remain on the training cluster.
172
+ W&B server readback verified 100 training points and all 72 per-panel evaluation
173
+ records (six panels × six checkpoints × two depths).
174
 
175
  ## Validation
176
 
177
+ Eleven local behavioral tests passed with Python 3.12, PyTorch 2.12.0 and
178
+ Transformers 5.9.0. Tests remain outside this model repository. They cover:
179
 
180
+ - Full-size parameter count on meta device and the 40-layer execution schedule.
181
+ - Exact K=1 initialization, logits and gradients versus the dense reference.
182
+ - K=2 outputs versus an independently unrolled network and summed shared gradients.
183
+ - Gradient checkpointing, model save/reload and optimizer continuation.
184
+ - Separate cache slots, chunked decoding and cached/uncached greedy generation.
185
+ - Depth-change rejection, invalid settings, packed-document isolation and padding.
186
+ - FlashAttention varlen dispatch at every execution, with external kernels stubbed.
 
 
187
 
188
+ The full-size GPU trial used SDPA, PyTorch
189
+ `2.12.0a0+0291f960b6.nv26.04.48445190`, CUDA 13.2 and Transformers 5.9.0.
190
+ Actual FlashAttention kernels and 128K training were not evaluated.
 
191
 
192
  ## Sources and licenses
193
 
194
+ - [MiniCPM5-1B configuration](https://huggingface.co/openbmb/MiniCPM5-1B/blob/87179e5c1f455ef22e6223592d2d61351b525bfc/config.json): base dimensions.
195
  - [Transformers 5.9.0 DeepSeek V4](https://github.com/huggingface/transformers/blob/v5.9.0/src/transformers/models/deepseek_v4/modeling_deepseek_v4.py): RMSNorm, dense MLP and interleaved RoPE.
196
  - Model implementation: Apache-2.0, see `LICENSE`.
197
  - DeepSeek tokenizer assets: MIT, see `LICENSE-TOKENIZER`.
198
 
199
+ No pretrained MiniCPM or DeepSeek weights are included or converted.
config.json CHANGED
@@ -19,7 +19,11 @@
19
  "intermediate_size": 4608,
20
  "max_position_embeddings": 131072,
21
  "mlp_bias": false,
22
- "model_type": "nano_dense",
 
 
 
 
23
  "num_attention_heads": 16,
24
  "num_hidden_layers": 24,
25
  "num_key_value_heads": 2,
 
19
  "intermediate_size": 4608,
20
  "max_position_embeddings": 131072,
21
  "mlp_bias": false,
22
+ "model_type": "nano_dense_loop",
23
+ "prelude_num_layers": 4,
24
+ "core_num_layers": 16,
25
+ "coda_num_layers": 4,
26
+ "num_core_iterations": 2,
27
  "num_attention_heads": 16,
28
  "num_hidden_layers": 24,
29
  "num_key_value_heads": 2,
configuration_nano_dense.py CHANGED
@@ -19,13 +19,17 @@ def _reject_unused(values):
19
 
20
  @strict
21
  class NanoDenseConfig(PreTrainedConfig):
22
- model_type = "nano_dense"
23
  keys_to_ignore_at_inference = ["past_key_values"]
24
 
25
  vocab_size: int = 129280
26
  hidden_size: int = 1536
27
  intermediate_size: int = 4608
28
  num_hidden_layers: int = 24
 
 
 
 
29
  num_attention_heads: int = 16
30
  num_key_value_heads: int = 2
31
  head_dim: int = 128
@@ -80,6 +84,11 @@ class NanoDenseConfig(PreTrainedConfig):
80
 
81
  def validate_architecture(self):
82
  _reject_unused(vars(self))
 
 
 
 
 
83
  for name in ("vocab_size", "hidden_size", "intermediate_size", "num_hidden_layers",
84
  "num_attention_heads", "num_key_value_heads", "head_dim", "max_position_embeddings"):
85
  value = getattr(self, name)
@@ -99,5 +108,13 @@ class NanoDenseConfig(PreTrainedConfig):
99
  if not math.isfinite(self.rms_norm_eps) or self.rms_norm_eps <= 0:
100
  raise ValueError("rms_norm_eps must be finite and positive")
101
 
 
 
 
 
 
 
 
 
102
 
103
  NanoDenseConfig.register_for_auto_class()
 
19
 
20
  @strict
21
  class NanoDenseConfig(PreTrainedConfig):
22
+ model_type = "nano_dense_loop"
23
  keys_to_ignore_at_inference = ["past_key_values"]
24
 
25
  vocab_size: int = 129280
26
  hidden_size: int = 1536
27
  intermediate_size: int = 4608
28
  num_hidden_layers: int = 24
29
+ prelude_num_layers: int = 4
30
+ core_num_layers: int = 16
31
+ coda_num_layers: int = 4
32
+ num_core_iterations: int = 2
33
  num_attention_heads: int = 16
34
  num_key_value_heads: int = 2
35
  head_dim: int = 128
 
84
 
85
  def validate_architecture(self):
86
  _reject_unused(vars(self))
87
+ for name in ('prelude_num_layers', 'core_num_layers', 'coda_num_layers', 'num_core_iterations'):
88
+ if type(getattr(self, name)) is not int or getattr(self, name) <= 0:
89
+ raise ValueError(f'{name} must be a positive integer')
90
+ if self.prelude_num_layers + self.core_num_layers + self.coda_num_layers != self.num_hidden_layers:
91
+ raise ValueError('prelude + core + coda must equal num_hidden_layers')
92
  for name in ("vocab_size", "hidden_size", "intermediate_size", "num_hidden_layers",
93
  "num_attention_heads", "num_key_value_heads", "head_dim", "max_position_embeddings"):
94
  value = getattr(self, name)
 
108
  if not math.isfinite(self.rms_norm_eps) or self.rms_norm_eps <= 0:
109
  raise ValueError("rms_norm_eps must be finite and positive")
110
 
111
+ def get_layer_schedule(self, num_core_iterations=None):
112
+ iterations = self.num_core_iterations if num_core_iterations is None else num_core_iterations
113
+ if type(iterations) is not int or iterations <= 0:
114
+ raise ValueError('num_core_iterations must be a positive integer')
115
+ start = self.prelude_num_layers
116
+ end = start + self.core_num_layers
117
+ return tuple(range(start)) + tuple(range(start, end)) * iterations + tuple(range(end, self.num_hidden_layers))
118
+
119
 
120
  NanoDenseConfig.register_for_auto_class()
count_parameters.py CHANGED
@@ -20,18 +20,27 @@ def parameter_breakdown(config):
20
  if __name__ == "__main__":
21
  parser = argparse.ArgumentParser(description=__doc__)
22
  parser.add_argument("--meta", action="store_true")
 
23
  args = parser.parse_args()
24
  root = Path(__file__).resolve().parent
25
  config = json.loads((root / "config.json").read_text())
 
 
 
 
26
  result = parameter_breakdown(config)
 
 
 
27
  result["bf16_kv_bytes_at_context_limit_batch1"] = (
28
- config["num_hidden_layers"] * 2 * config["num_key_value_heads"]
29
  * config["head_dim"] * 2 * config["max_position_embeddings"]
30
  )
31
  if args.meta:
32
  import torch
33
  from transformers import AutoConfig, AutoModelForCausalLM
34
  cfg = AutoConfig.from_pretrained(root, trust_remote_code=True)
 
35
  with torch.device("meta"):
36
  model = AutoModelForCausalLM.from_config(cfg, trust_remote_code=True)
37
  actual = sum(p.numel() for p in model.parameters())
 
20
  if __name__ == "__main__":
21
  parser = argparse.ArgumentParser(description=__doc__)
22
  parser.add_argument("--meta", action="store_true")
23
+ parser.add_argument("--core-iterations", type=int, default=None)
24
  args = parser.parse_args()
25
  root = Path(__file__).resolve().parent
26
  config = json.loads((root / "config.json").read_text())
27
+ iterations = config["num_core_iterations"] if args.core_iterations is None else args.core_iterations
28
+ if iterations <= 0:
29
+ parser.error("--core-iterations must be positive")
30
+ effective_layers = config["prelude_num_layers"] + config["core_num_layers"] * iterations + config["coda_num_layers"]
31
  result = parameter_breakdown(config)
32
+ result["physical_layers"] = config["num_hidden_layers"]
33
+ result["core_iterations"] = iterations
34
+ result["effective_layers"] = effective_layers
35
  result["bf16_kv_bytes_at_context_limit_batch1"] = (
36
+ effective_layers * 2 * config["num_key_value_heads"]
37
  * config["head_dim"] * 2 * config["max_position_embeddings"]
38
  )
39
  if args.meta:
40
  import torch
41
  from transformers import AutoConfig, AutoModelForCausalLM
42
  cfg = AutoConfig.from_pretrained(root, trust_remote_code=True)
43
+ cfg.num_core_iterations = iterations
44
  with torch.device("meta"):
45
  model = AutoModelForCausalLM.from_config(cfg, trust_remote_code=True)
46
  actual = sum(p.numel() for p in model.parameters())
modeling_nano_dense.py CHANGED
@@ -3,6 +3,8 @@
3
  # Licensed under the Apache License, Version 2.0.
4
  """Nano dense decoder using DeepSeek V4 normalization, MLP and interleaved RoPE."""
5
 
 
 
6
  import torch
7
  from torch import nn
8
  from transformers import initialization as init
@@ -22,6 +24,15 @@ from transformers.utils.output_capturing import capture_outputs
22
  from .configuration_nano_dense import NanoDenseConfig
23
 
24
 
 
 
 
 
 
 
 
 
 
25
  def _document_boundaries(position_ids, attention_mask):
26
  boundaries = position_ids[:, 1:] != position_ids[:, :-1] + 1
27
  if attention_mask is not None:
@@ -57,7 +68,7 @@ class NanoDenseAttention(nn.Module):
57
  self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False)
58
 
59
  def forward(self, hidden_states, position_embeddings, attention_mask=None,
60
- past_key_values=None, **kwargs):
61
  batch, length, _ = hidden_states.shape
62
  shape = (batch, length, -1, self.head_dim)
63
  query = self.q_proj(hidden_states).view(shape).transpose(1, 2)
@@ -67,7 +78,7 @@ class NanoDenseAttention(nn.Module):
67
  query = apply_rotary_pos_emb(query, cos, sin)
68
  key = apply_rotary_pos_emb(key, cos, sin)
69
  if past_key_values is not None:
70
- key, value = past_key_values.update(key, value, self.layer_idx)
71
  backend = ALL_ATTENTION_FUNCTIONS.get_interface(
72
  self.config._attn_implementation, eager_attention_forward
73
  )
@@ -132,12 +143,30 @@ class NanoDenseModel(NanoDensePreTrainedModel):
132
  self.gradient_checkpointing = False
133
  self.post_init()
134
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  @merge_with_config_defaults
136
  @capture_outputs
137
  def forward(self, input_ids=None, attention_mask=None, position_ids=None,
138
- past_key_values=None, inputs_embeds=None, use_cache=None, **kwargs):
 
139
  if (input_ids is None) == (inputs_embeds is None):
140
  raise ValueError("Specify exactly one of input_ids and inputs_embeds")
 
 
 
 
 
141
  if inputs_embeds is None:
142
  inputs_embeds = self.embed_tokens(input_ids)
143
  if self.training:
@@ -173,19 +202,20 @@ class NanoDenseModel(NanoDensePreTrainedModel):
173
  raise ValueError("Packed documents with padding are unsupported; use padding-free packing")
174
  attention_mask = None
175
  use_cache = False
176
- if use_cache and past_key_values is None:
177
- past_key_values = DynamicCache(config=self.config)
178
  mask = create_causal_mask(
179
  config=self.config, inputs_embeds=inputs_embeds, attention_mask=attention_mask,
180
  past_key_values=past_key_values, position_ids=position_ids,
181
  )
182
  position_embeddings = self.rotary_emb(inputs_embeds, position_ids, layer_type="main")
183
  hidden_states = inputs_embeds
184
- for layer in self.layers:
 
185
  hidden_states = layer(
186
  hidden_states, attention_mask=mask, position_ids=position_ids,
187
  position_embeddings=position_embeddings, past_key_values=past_key_values,
188
- use_cache=use_cache, **kwargs,
189
  )
190
  return BaseModelOutputWithPast(
191
  last_hidden_state=self.norm(hidden_states), past_key_values=past_key_values,
@@ -205,13 +235,13 @@ class NanoDenseForCausalLM(NanoDensePreTrainedModel, GenerationMixin):
205
  @can_return_tuple
206
  def forward(self, input_ids=None, attention_mask=None, position_ids=None,
207
  past_key_values=None, inputs_embeds=None, labels=None, use_cache=None,
208
- logits_to_keep=0, **kwargs):
209
  if labels is not None and not (isinstance(logits_to_keep, int) and logits_to_keep == 0):
210
  raise ValueError("Training with labels requires all logits (logits_to_keep=0)")
211
  outputs = self.model(
212
  input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids,
213
  past_key_values=past_key_values, inputs_embeds=inputs_embeds,
214
- use_cache=use_cache, **kwargs,
215
  )
216
  indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
217
  logits = self.lm_head(outputs.last_hidden_state[:, indices, :])
@@ -228,6 +258,11 @@ class NanoDenseForCausalLM(NanoDensePreTrainedModel, GenerationMixin):
228
  hidden_states=outputs.hidden_states, attentions=outputs.attentions,
229
  )
230
 
 
 
 
 
 
231
 
232
  NanoDenseModel.register_for_auto_class("AutoModel")
233
  NanoDenseForCausalLM.register_for_auto_class("AutoModelForCausalLM")
 
3
  # Licensed under the Apache License, Version 2.0.
4
  """Nano dense decoder using DeepSeek V4 normalization, MLP and interleaved RoPE."""
5
 
6
+ from copy import copy
7
+
8
  import torch
9
  from torch import nn
10
  from transformers import initialization as init
 
24
  from .configuration_nano_dense import NanoDenseConfig
25
 
26
 
27
+ class NanoDenseLoopCache(DynamicCache):
28
+ def __init__(self, config, num_core_iterations):
29
+ self.num_core_iterations = num_core_iterations
30
+ self.physical_layer_indices = config.get_layer_schedule(num_core_iterations)
31
+ cache_config = copy(config)
32
+ cache_config.num_hidden_layers = len(self.physical_layer_indices)
33
+ super().__init__(config=cache_config)
34
+
35
+
36
  def _document_boundaries(position_ids, attention_mask):
37
  boundaries = position_ids[:, 1:] != position_ids[:, :-1] + 1
38
  if attention_mask is not None:
 
68
  self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False)
69
 
70
  def forward(self, hidden_states, position_embeddings, attention_mask=None,
71
+ past_key_values=None, cache_layer_idx=None, **kwargs):
72
  batch, length, _ = hidden_states.shape
73
  shape = (batch, length, -1, self.head_dim)
74
  query = self.q_proj(hidden_states).view(shape).transpose(1, 2)
 
78
  query = apply_rotary_pos_emb(query, cos, sin)
79
  key = apply_rotary_pos_emb(key, cos, sin)
80
  if past_key_values is not None:
81
+ key, value = past_key_values.update(key, value, cache_layer_idx)
82
  backend = ALL_ATTENTION_FUNCTIONS.get_interface(
83
  self.config._attn_implementation, eager_attention_forward
84
  )
 
143
  self.gradient_checkpointing = False
144
  self.post_init()
145
 
146
+ def _prepare_loop_cache(self, cache, iterations, schedule):
147
+ if isinstance(cache, NanoDenseLoopCache):
148
+ if cache.num_core_iterations != iterations or cache.physical_layer_indices != schedule:
149
+ raise ValueError('Cannot change iterations or layer schedule of an existing cache')
150
+ return cache
151
+ if cache is not None:
152
+ if type(cache) is not DynamicCache or cache.offloading or any(
153
+ layer.get_seq_length() != 0 for layer in cache.layers
154
+ ):
155
+ raise ValueError('Use an empty DynamicCache or the returned loop cache')
156
+ return NanoDenseLoopCache(self.config, iterations)
157
+
158
  @merge_with_config_defaults
159
  @capture_outputs
160
  def forward(self, input_ids=None, attention_mask=None, position_ids=None,
161
+ past_key_values=None, inputs_embeds=None, use_cache=None,
162
+ num_core_iterations=None, **kwargs):
163
  if (input_ids is None) == (inputs_embeds is None):
164
  raise ValueError("Specify exactly one of input_ids and inputs_embeds")
165
+ if num_core_iterations is None:
166
+ num_core_iterations = (past_key_values.num_core_iterations
167
+ if isinstance(past_key_values, NanoDenseLoopCache)
168
+ else self.config.num_core_iterations)
169
+ schedule = self.config.get_layer_schedule(num_core_iterations)
170
  if inputs_embeds is None:
171
  inputs_embeds = self.embed_tokens(input_ids)
172
  if self.training:
 
202
  raise ValueError("Packed documents with padding are unsupported; use padding-free packing")
203
  attention_mask = None
204
  use_cache = False
205
+ if use_cache:
206
+ past_key_values = self._prepare_loop_cache(past_key_values, num_core_iterations, schedule)
207
  mask = create_causal_mask(
208
  config=self.config, inputs_embeds=inputs_embeds, attention_mask=attention_mask,
209
  past_key_values=past_key_values, position_ids=position_ids,
210
  )
211
  position_embeddings = self.rotary_emb(inputs_embeds, position_ids, layer_type="main")
212
  hidden_states = inputs_embeds
213
+ for execution_index, physical_index in enumerate(schedule):
214
+ layer = self.layers[physical_index]
215
  hidden_states = layer(
216
  hidden_states, attention_mask=mask, position_ids=position_ids,
217
  position_embeddings=position_embeddings, past_key_values=past_key_values,
218
+ cache_layer_idx=execution_index, use_cache=use_cache, **kwargs,
219
  )
220
  return BaseModelOutputWithPast(
221
  last_hidden_state=self.norm(hidden_states), past_key_values=past_key_values,
 
235
  @can_return_tuple
236
  def forward(self, input_ids=None, attention_mask=None, position_ids=None,
237
  past_key_values=None, inputs_embeds=None, labels=None, use_cache=None,
238
+ logits_to_keep=0, num_core_iterations=None, **kwargs):
239
  if labels is not None and not (isinstance(logits_to_keep, int) and logits_to_keep == 0):
240
  raise ValueError("Training with labels requires all logits (logits_to_keep=0)")
241
  outputs = self.model(
242
  input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids,
243
  past_key_values=past_key_values, inputs_embeds=inputs_embeds,
244
+ use_cache=use_cache, num_core_iterations=num_core_iterations, **kwargs,
245
  )
246
  indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
247
  logits = self.lm_head(outputs.last_hidden_state[:, indices, :])
 
258
  hidden_states=outputs.hidden_states, attentions=outputs.attentions,
259
  )
260
 
261
+ def _prepare_cache_for_generation(self, generation_config, *args, **kwargs):
262
+ if generation_config.cache_implementation not in (None, 'dynamic'):
263
+ raise ValueError("Loop generation supports only cache_implementation='dynamic'")
264
+ return super()._prepare_cache_for_generation(generation_config, *args, **kwargs)
265
+
266
 
267
  NanoDenseModel.register_for_auto_class("AutoModel")
268
  NanoDenseForCausalLM.register_for_auto_class("AutoModelForCausalLM")