bobbydeveloper2014 commited on
Commit
4d74557
Β·
verified Β·
1 Parent(s): 3d0d0db

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ pipeline_tag: text-generation
6
+ tags:
7
+ - hybrid-ssm
8
+ - mha
9
+ - moe
10
+ - state-space-models
11
+ - recurrent-gated-expert
12
+ - arg
13
+ - hse
14
+ - b300
15
+ - runpod
16
+ - instruct
17
+ - chat
18
+ base_model: Phase-Technologies/forge-500m-base
19
+ ---
20
+
21
+ # FORGE-500M-Chat: First-Principles Optimal Recurrent-Gated Expert Architecture
22
+
23
+ <div align="center">
24
+
25
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
26
+ [![Parameters](https://img.shields.io/badge/Parameters-500M%20Total%20%7C%20320M%20Active-green.svg)]()
27
+ [![Hardware](https://img.shields.io/badge/Trained%20On-4%C3%97%20NVIDIA%20B300%20SXM6-76B900.svg?logo=nvidia)]()
28
+ [![Architecture](https://img.shields.io/badge/Architecture-Hybrid%20SSM%20%2B%20MHA%20%2B%20HSE--MoE-orange.svg)]()
29
+ [![Training Tokens](https://img.shields.io/badge/Tokens-1.66B%20Pretrain%20%2B%20Aligned-purple.svg)]()
30
+
31
+ </div>
32
+
33
+ ---
34
+
35
+ ## Executive Summary
36
+
37
+ **FORGE-500M-Chat** is a state-of-the-art hybrid sub-billion parameter conversational language model designed from first principles by **Soham Pal**. It solves the compute accessibility gap in AI research by replacing quadratic self-attention with linear-time recurrent state-space recurrence, paired with hierarchical sparse expert mixture-of-experts (HSE-MoE).
38
+
39
+ The model was pretrained on **1.66 Billion tokens** from scratch across 4Γ— NVIDIA B300 SXM6 MIG compute slices, aligned via **Supervised Fine-Tuning (SFT)** on multi-turn dialogue with per-token loss masking, and polished using **Direct Preference Optimization (DPO)**.
40
+
41
+ The full console training trajectory from Step 1 to Step 821 is archived in this repository under [`training_journey.log`](./training_journey.log).
42
+
43
+ ---
44
+
45
+ ## Model Architecture Specifications
46
+
47
+ FORGE employs an asymmetric, multi-layer hybrid topology designed for maximum parameter utility and linear sequence scalability:
48
+
49
+ | Specification | Value | Technical Description |
50
+ | :--- | :--- | :--- |
51
+ | **Total Parameters** | **500,019,616** | 500.0M total weights (FP16/BF16 storage: ~1.0 GB) |
52
+ | **Active Parameters** | **~320,000,000** | Active per-token forward pass via Sparse HSE-MoE |
53
+ | **Embedding Size** | 211,444,736 | 206,464 vocabulary Γ— 1,024 hidden dimension |
54
+ | **Hidden Dimension ($d_{\text{model}}$)** | 1,024 | Intermediate representation width |
55
+ | **Total Transformer Layers** | 18 | Interleaved hybrid recurrence + global attention |
56
+ | **SSM Recurrence Layers** | 15 (83.3%) | **Adaptive Recurrent Gating (ARG)** linear state space |
57
+ | **Attention Layers** | 3 (16.7%) | **Global Multi-Head Attention (MHA)** with RoPE + YaRN |
58
+ | **Attention Heads** | 8 | 8 Query heads, 8 Key/Value heads |
59
+ | **MoE FFN Layers** | 9 | **Hierarchical Sparse Expert (HSE)** (32 experts, top-2 active) |
60
+ | **Dense FFN Layers** | 9 | Dense SwiGLU FFN with $8/3 \times d_{\text{model}}$ expansion |
61
+ | **Context Window** | 4,096 | Native context (scalable to 16,384 with YaRN) |
62
+ | **Tokenizer** | CRAYON Turbo | 206,464 vocabulary (BPE with subword byte-fallback) |
63
+
64
+ ```
65
+ Input Tokens (B, T)
66
+ β”‚
67
+ β–Ό
68
+ [CRAYON Embedding (206k Vocab, 1024 d_model)]
69
+ β”‚
70
+ β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
71
+ β”‚ Layer 0..17: Interleaved Blocks β”‚
72
+ β”‚ β”œβ”€ 15Γ— ARG Layers: Linear SSM Recurrence β”‚
73
+ β”‚ β”œβ”€ 3Γ— Global MHA: Long-Range Attention β”‚
74
+ β”‚ β”œβ”€ 9Γ— SwiGLU Dense FFN β”‚
75
+ β”‚ └─ 9Γ— Hierarchical Sparse Expert (MoE) β”‚
76
+ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
77
+ β–Ό
78
+ [Final RMSNorm + Chunked LM Projection Head]
79
+ β”‚
80
+ β–Ό
81
+ Target Logits / Probabilities
82
+ ```
83
+
84
+ ---
85
+
86
+ ## The Full Training Journey & Historical Narrative
87
+
88
+ The pretraining and post-training of FORGE-500M represents an empirical milestone in lean, high-efficiency AI development:
89
+
90
+ ### 1. The Pretraining Stage
91
+ - **Cluster**: 4Γ— NVIDIA B300 SXM6 MIG slices (`1g.34gb`, 18 SMs each, CUDA 13.2, Driver 595.91).
92
+ - **Global Batch**: 1,048,576 tokens per step (32 micro-batches Γ— 32,768 tokens per batch).
93
+ - **Physical Throughput**: 7,337 unique tokens per second (~26.4 Million tokens/hour).
94
+ - **Step 1**: Initial Cross-Entropy Loss: **12.512**, Initial Perplexity: **> 150,000**.
95
+ - **Step 821**: Final Loss: **4.6473**, Final Perplexity: **104.30**.
96
+ - **Cumulative Pretrain Tokens**: **1,660,944,384 tokens** (1.66 Billion tokens).
97
+ - **Challenges Overcome**:
98
+ - Survived multiple cloud host network disruptions and PCIe MIG interface resets via resilient checkpoint auto-recovery.
99
+ - Successfully resolved PyTorch 2.5 `nn.Module` dynamic attribute restrictions with custom parameter wrapping (`convert_to_attr_dict`).
100
+ - Implemented chunked linear projection (`chunk_size=1024`) on the 206,464-token vocabulary to eliminate VRAM allocation spikes.
101
+
102
+ ### 2. Supervised Fine-Tuning (SFT)
103
+ - **Dataset**: Curated mixture of ShareGPT dialogues, UltraChat multi-turn instructions, CodeFeedback Python programming pairs, MetaMath mathematical reasoning, and OpenOrca synthetic data.
104
+ - **Protocol**: Chat template formatting with assistant turn loss masking (no loss on system or user prompts).
105
+ - **Optimization**: PyTorch DistributedDataParallel (DDP) across all 4 GPUs with cosine learning rate decay ($2\times 10^{-5} \to 2\times 10^{-6}$) and BF16 autocast.
106
+ - **Result**: Loss dropped rapidly to **~4.0**, establishing clean turn-taking and instruction responsiveness.
107
+
108
+ ### 3. Direct Preference Optimization (DPO)
109
+ - **Dataset**: Multi-domain preference pairs from UltraFeedback, Anthropic HH-RLHF, and HelpSteer2.
110
+ - **Protocol**: Trainable policy model aligned against frozen BF16 SFT reference model with KL divergence regularization ($\beta = 0.1$).
111
+ - **Result**: Achieved positive reward margin on chosen vs. rejected completions, eliminating repetitive degradation and improving refusal of hazardous requests.
112
+
113
+ ---
114
+
115
+ ## Complete Training Console Logs Preserved
116
+
117
+ In accordance with our commitment to total scientific reproducibility, the complete console log file (`training_journey.log`) containing every training step from Step 1 to Step 821, memory footprints, step durations, and loss curves is provided directly in the repository root:
118
+
119
+ - **Filename**: [`training_journey.log`](./training_journey.log)
120
+ - **Size**: ~5.9 MB
121
+ - **Coverage**: Steps 1 through 821, SFT, and DPO alignment logs.
122
+
123
+ ---
124
+
125
+ ## How to Run Inference
126
+
127
+ ### 1. Google Colab (1-Click Quickstart)
128
+
129
+ In a Google Colab notebook (T4 / A100 GPU), paste and run this single cell:
130
+
131
+ ```bash
132
+ # 1. Clone repository & enter directory
133
+ !git clone https://huggingface.co/Phase-Technologies/FORGE-500M-Chat
134
+ %cd FORGE-500M-Chat
135
+
136
+ # 2. Install dependencies
137
+ !pip install -q torch einops triton transformers huggingface_hub
138
+
139
+ # 3. Generate text immediately
140
+ !python run_inference.py --prompt "<|bos|><|usr|>Explain how quantum computers differ from classical computers.<|asst|>" --max_new_tokens 128
141
+ ```
142
+
143
+ ---
144
+
145
+ ### 2. Pure Python API (Colab / Jupyter / Local)
146
+
147
+ ```python
148
+ import sys
149
+ import torch
150
+ from pathlib import Path
151
+ from huggingface_hub import snapshot_download
152
+
153
+ # 1. Download model repository
154
+ repo_dir = Path(snapshot_download(repo_id="Phase-Technologies/FORGE-500M-Chat"))
155
+ sys.path.insert(0, str(repo_dir))
156
+
157
+ from tokenizer.crayon_wrapper import ForgeTokenizer
158
+ from config import ForgeModelConfig
159
+ from model.forge_model import build_forge_500m
160
+
161
+ tokenizer = ForgeTokenizer.from_pretrained(str(repo_dir / "tokenizer"))
162
+ config = ForgeModelConfig.from_json(str(repo_dir / "model_config.json"))
163
+ model = build_forge_500m(config)
164
+
165
+ # 3. Load BF16 weights
166
+ weights = torch.load(repo_dir / "pytorch_model.bin", map_location="cpu")
167
+ model.load_state_dict({k: v.to(torch.bfloat16) for k, v in weights.items()})
168
+ model = model.to("cuda" if torch.cuda.is_available() else "cpu").to(torch.bfloat16)
169
+ model.eval()
170
+
171
+ # 4. Formatted Chat Prompt
172
+ prompt = "<|bos|><|usr|> Explain how quantum computers differ from classical computers in simple terms.<|asst|>"
173
+
174
+ input_ids = torch.tensor([tokenizer.encode(prompt, add_bos=False, add_eos=False)], device=model.device)
175
+
176
+ # 5. Autoregressive Generation
177
+ with torch.inference_mode():
178
+ output_ids = model.generate(
179
+ input_ids=input_ids,
180
+ max_new_tokens=128,
181
+ temperature=0.7,
182
+ top_p=0.9,
183
+ eos_token_id=tokenizer.special_tokens["<|eos|>"],
184
+ )
185
+
186
+ print(tokenizer.decode(output_ids[0].tolist(), skip_special_tokens=True))
187
+ ```
188
+
189
+ ### 3. Special Conversation Tokens
190
+
191
+ | Token | String | Function |
192
+ | :--- | :--- | :--- |
193
+ | **BOS** | `<|bos|>` | Beginning of sequence |
194
+ | **EOS** | `<|eos|>` | End of sequence / turn termination |
195
+ | **SYS** | `<|sys|>` | System instruction block prefix |
196
+ | **USR** | `<|usr|>` | User prompt prefix |
197
+ | **ASST** | `<|asst|>` | Assistant completion prefix |
198
+ | **PAD** | `<|pad|>` | Sequence padding token |
199
+
200
+ ---
201
+
202
+ ## a note from the creator.....
203
+
204
+ honestly i really wanted to train this beauty for full 15b tokens..... that was the real plan from day one to see how far this custom architecture could push..... but running multiple top tier gpus burns money so fast and i just ran completely out of budget..... literally had to stop pretraining early at 821 steps..... then quickly did sft and dpo right on the pod before shutting it down..... huge shoutout and love to runpod.io for providing the insane compute and infrastructure that made training this beast possible..... every single cent went into this little 500m model and seeing it come alive from scratch with custom cuda kernels and our own tokenizer was pure magic..... maybe one day with better funding we will train the full 3b or 15b token run..... until then i hope you guys enjoy playing with this model as much as i loved building it..... thank you so much to everyone who supported this crazy journey.....
205
+
config.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORGE-3B: Master Configuration
3
+ All hyperparameters, paths, and GPU optimization settings in one place.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import os
8
+ import json
9
+ import math
10
+ from dataclasses import dataclass, field, asdict
11
+ from typing import Optional, List, Literal
12
+ from pathlib import Path
13
+
14
+
15
+ # ─────────────────────────────────────────────────────────────────────────────
16
+ # GPU GLOBAL OPTIMIZATION FLAGS
17
+ # These are applied at import time across the entire process.
18
+ # ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ import torch
21
+
22
+ # Allow TF32 for matmuls and convolutions β€” massive speedup on Ampere/Hopper
23
+ # with negligible precision loss for language model training.
24
+ if torch.cuda.is_available():
25
+ torch.backends.cuda.matmul.allow_tf32 = True
26
+ torch.backends.cudnn.allow_tf32 = True
27
+ torch.backends.cudnn.benchmark = True # auto-tune cuDNN kernels
28
+ try:
29
+ torch.backends.cuda.enable_flash_sdp(True) # Flash SDP in PyTorch 2.x
30
+ torch.backends.cuda.enable_mem_efficient_sdp(True)
31
+ torch.backends.cuda.enable_math_sdp(False) # disable slow math SDP
32
+ except Exception:
33
+ pass
34
+
35
+ # NCCL tuning for 64x H100 GPU cluster (8x 8-GPU nodes via NVLink + 3.2 Tbps InfiniBand)
36
+ os.environ.setdefault("NCCL_MIN_NCHANNELS", "32")
37
+ os.environ.setdefault("NCCL_MAX_NCHANNELS", "64")
38
+ os.environ.setdefault("NCCL_NSOCKS_PERTHREAD", "8")
39
+ os.environ.setdefault("NCCL_SOCKET_NTHREADS", "8")
40
+ os.environ.setdefault("NCCL_BUFFSIZE", "16777216") # 16MB ring buffer for high-bandwidth AllReduce
41
+ os.environ.setdefault("NCCL_NET_GDR_LEVEL", "5") # GPUDirect RDMA Level 5 for NVLink + InfiniBand
42
+ os.environ.setdefault("NCCL_CROSS_NIC", "1") # Multi-NIC rail-optimized routing
43
+ os.environ.setdefault("NCCL_IB_DISABLE", "0") # InfiniBand enabled
44
+ os.environ.setdefault("NCCL_P2P_DISABLE", "0") # NVLink P2P enabled
45
+ os.environ.setdefault("NCCL_NVLS_ENABLE", "1") # NVLink SHARP collective offload
46
+ os.environ.setdefault("CUDA_DEVICE_MAX_CONNECTIONS", "1") # Precise CUDA stream scheduling
47
+
48
+ # CUDA memory allocator β€” reduce fragmentation for large model + optimizer states
49
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF",
50
+ "expandable_segments:True,max_split_size_mb:128,garbage_collection_threshold:0.8")
51
+
52
+ # Tokenizer parallelism β€” disable HF tokenizer parallelism (we use CRAYON)
53
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
54
+
55
+
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+ # MODEL CONFIGURATION
58
+ # ─────────────────────────────────────────────────────────────────────────────
59
+
60
+ @dataclass
61
+ class ForgeModelConfig:
62
+ """Complete FORGE-500M architecture configuration. 500.02M total params."""
63
+
64
+ # ── Core Dimensions ──────────────────────────────────────────────────────
65
+ vocab_size: int = 206_464 # CRAYON standard profile (206,373) padded to Γ—128
66
+ d_model: int = 1024 # 500M: 1024 (down from 1280)
67
+ n_layers: int = 18 # 500M: 18 layers (15 ARG + 3 MHA)
68
+ max_seq_len: int = 4096
69
+
70
+ # ── Layer Distribution ────────────────────────────────────────────────────
71
+ # Layer 0-4 = ARG; Layer 5 = MHA; repeat Γ—3 across 18 layers
72
+ mha_layer_indices: List[int] = field(default_factory=lambda: [5, 11, 17])
73
+ # Dense FFN on odd layers, HSE on even layers
74
+ dense_ffn_layer_indices: List[int] = field(default_factory=lambda: list(range(1, 18, 2)))
75
+ hse_ffn_layer_indices: List[int] = field(default_factory=lambda: list(range(0, 18, 2)))
76
+
77
+ # ── ARG (Adaptive Recurrent Gating) ──────────────────────────────────────
78
+ arg_d_inner: int = 1024 # matches d_model for 500M
79
+ arg_d_state: int = 48
80
+ arg_d_rank: int = 48 # dt projection rank
81
+ arg_conv_kernel: int = 4
82
+ arg_local_window: int = 64 # local attention window (phase 1+2)
83
+ arg_local_n_heads: int = 8
84
+ arg_local_n_kv_heads: int = 2
85
+ arg_head_dim: int = 64 # 1024 / 16 heads
86
+ arg_scalar_gate: bool = True # scalar gate per token (vs d_model-dim)
87
+
88
+ # ── MHA (Multi-Head Attention) ────────────────────────────────────────────
89
+ mha_n_heads: int = 16
90
+ mha_n_kv_heads: int = 4 # GQA: 4:1 ratio
91
+ mha_head_dim: int = 64 # 1024 / 16 heads
92
+ rope_theta: float = 500_000.0 # long-context RoPE base
93
+ rope_scaling_type: Optional[str] = None # None | "yarn"
94
+ rope_scaling_factor: float = 1.0
95
+
96
+ # ── Dense FFN (SwiGLU) ────────────────────────────────────────────────────
97
+ dense_d_ff: int = 2560 # 2.5 Γ— d_model, multiple of 128
98
+
99
+ # ── HSE FFN (Hierarchical Sparse Expert) ──────────────────────────────────
100
+ hse_n_domains: int = 4
101
+ hse_n_experts_per_domain: int = 6 # total 24 experts (down from 32)
102
+ hse_top_k: int = 2 # active per token
103
+ hse_d_ff_expert: int = 210 # scaled for 500M total params
104
+ hse_capacity_factor: float = 1.25
105
+ hse_expert_dropout: float = 0.1
106
+ hse_aux_loss_alpha: float = 0.01
107
+ hse_gumbel_tau_init: float = 1.0
108
+ hse_gumbel_tau_final: float = 0.1
109
+ hse_gumbel_tau_warmup_tokens: int = 10_000_000_000 # anneal over 10B tokens
110
+
111
+ # ── Normalization ─────────────────────────────────────────────────────────
112
+ norm_type: str = "dgn" # "dgn" or "rmsnorm"
113
+ dgn_n_groups: int = 16 # 1024 / 16 = 64 per group
114
+ norm_eps: float = 1e-6
115
+
116
+ # ── Initialization ────────────────────────────────────────────────────────
117
+ init_std: float = 0.02
118
+ init_std_residual_scale: bool = True # scale by 1/sqrt(2n_layers)
119
+
120
+ # ── Misc ──────────────────────────────────────────────────────────────────
121
+ dropout: float = 0.0 # 0.0 during pretraining
122
+ tie_word_embeddings: bool = True
123
+ use_cache: bool = True # KV cache for inference
124
+ pad_token_id: int = 0
125
+ bos_token_id: int = 1
126
+ eos_token_id: int = 2
127
+
128
+ # ── Compile/Optimization Flags ────────────────────────────────────
129
+ use_flash_attention: bool = True
130
+ use_triton_kernels: bool = True # Triton fused ops
131
+ use_torch_compile: bool = True
132
+ compile_mode: str = "max-autotune" # max throughput on H100
133
+ use_gradient_checkpointing: bool = False # 500M fits easily in 80GB without checkpointing
134
+ gradient_checkpointing_ratio: float = 0.0
135
+
136
+ def n_params_total(self) -> int:
137
+ """Approximate total parameter count."""
138
+ embed = self.vocab_size * self.d_model
139
+ n_arg = len([i for i in range(self.n_layers) if i not in self.mha_layer_indices])
140
+ n_mha = len(self.mha_layer_indices)
141
+ n_dense = len(self.dense_ffn_layer_indices)
142
+ n_hse = len(self.hse_ffn_layer_indices)
143
+
144
+ arg_per = (
145
+ self.d_model * 2 * self.arg_d_inner + # in_proj
146
+ self.arg_d_inner * self.arg_conv_kernel + # conv1d
147
+ self.arg_d_inner * (self.arg_d_rank + 2 * self.arg_d_state) + # x_proj
148
+ self.arg_d_rank * self.arg_d_inner + # dt_proj
149
+ self.arg_d_state * 2 + # nu, theta
150
+ self.arg_d_inner + # D
151
+ self.arg_d_inner * self.d_model + # out_proj
152
+ self.arg_local_n_heads * self.arg_head_dim * self.d_model + # Q
153
+ self.arg_local_n_kv_heads * self.arg_head_dim * self.d_model * 2 + # KV
154
+ self.arg_local_n_heads * self.arg_head_dim * self.d_model + # O
155
+ self.d_model * self.arg_d_state # CPB
156
+ )
157
+
158
+ mha_per = (
159
+ self.d_model * self.mha_n_heads * self.mha_head_dim + # Q
160
+ self.d_model * self.mha_n_kv_heads * self.mha_head_dim * 2 + # KV
161
+ self.d_model * self.mha_n_heads * self.mha_head_dim # O
162
+ )
163
+
164
+ dense_per = 3 * self.d_model * self.dense_d_ff
165
+
166
+ n_experts_total = self.hse_n_domains * self.hse_n_experts_per_domain
167
+ hse_per = n_experts_total * 3 * self.d_model * self.hse_d_ff_expert
168
+
169
+ total = embed + n_arg * arg_per + n_mha * mha_per + n_dense * dense_per + n_hse * hse_per
170
+ return total
171
+
172
+ def to_json(self, path: str):
173
+ with open(path, "w") as f:
174
+ json.dump(asdict(self), f, indent=2)
175
+
176
+ @classmethod
177
+ def from_json(cls, path: str) -> "ForgeModelConfig":
178
+ import inspect
179
+ with open(path) as f:
180
+ data = json.load(f)
181
+ sig = inspect.signature(cls)
182
+ filtered_data = {k: v for k, v in data.items() if k in sig.parameters}
183
+ return cls(**filtered_data)
184
+
185
+
186
+ # ─────────────────────────────────────────────────────────────────────────────
187
+ # TRAINING CONFIGURATION
188
+ # ─────────────────────────────────────────────────────────────────────────────
189
+
190
+ @dataclass
191
+ class PretrainConfig:
192
+ """Phase-aware pretraining configuration for FORGE-500M.
193
+
194
+ Budget math (1x H100 SXM @ $3.29/hr):
195
+ - Phase 1 (vocab warmup): 2B tokens @ 80k tok/s β†’ ~7h β†’ $23
196
+ - Phase 2 (core pretrain): 16B tokens @ 80k tok/s β†’ ~56h β†’ $184
197
+ - Phase 3 (ctx extension): 2B tokens @ 60k tok/s β†’ ~9h β†’ $30
198
+ PRETRAIN TOTAL: ~72h β†’ ~$237
199
+ SFT: 1.4B tokens β†’ ~5h β†’ $16
200
+ DPO: 0.8B tokens β†’ ~3h β†’ $10
201
+ GRAND TOTAL: ~$263 of $400 budget. $137 buffer.
202
+ """
203
+
204
+ output_dir: str = "./checkpoints/forge_500m_pretrain"
205
+ data_dir: str = "Phase-Technologies/forge-3b-pretrain-data" # same tokenized data
206
+ resume_from_checkpoint: Optional[str] = None
207
+
208
+ # ── Token Budget (15B Tokens for 500M params: ~$245.59 total cost @ $3.50/GPU/hr on 32x H100) ──
209
+ # 15B tokens directly in Phase 2 at seq=2048 (Phase 1 & Phase 3 omitted)
210
+ phase1_tokens: int = 0 # Omitted (start directly on Phase 2 @ seq=2048)
211
+ phase2_tokens: int = 15_000_000_000 # 15.0B core pretrain (seq=2048) -> ~2.08h ($233.33)
212
+ phase3_tokens: int = 0 # Omitted (pure seq=2048 pretraining)
213
+ total_tokens: int = 15_000_000_000 # 15.0B total pretrain
214
+
215
+ # ── Batch Config (Tuned for 32x H100 SXM β€” 1,048,576 tokens/step) ─────────
216
+ # With 32 GPUs @ micro_batch=16 (seq=2048): 32 Γ— 16 Γ— 2048 = 1,048,576 tokens per pass!
217
+ # Exactly 1 gradient accumulation step (zero idle wait, maximum NVLink saturation)
218
+ phase1_global_batch_tokens: int = 1_048_576 # 1M tokens/step
219
+ phase2_global_batch_tokens: int = 1_048_576 # 1M tokens/step (Phase 2)
220
+ phase3_global_batch_tokens: int = 1_048_576 # 1M tokens/step (Phase 3)
221
+ micro_batch_size_per_gpu: int = 16 # default fallback
222
+ phase1_micro_batch_per_gpu: int = 64 # 64 seqs Γ— 512 = 32,768 tokens/GPU
223
+ phase2_micro_batch_per_gpu: int = 16 # 16 seqs Γ— 2048 = 32,768 tokens/GPU (1 accum step across 32 GPUs)
224
+ phase3_micro_batch_per_gpu: int = 8 # 8 seqs Γ— 4096 = 32,768 tokens/GPU
225
+
226
+ # ── Context Lengths ───────────────────────────────────────────────────────
227
+ phase1_seq_len: int = 512
228
+ phase2_seq_len: int = 2048
229
+ phase3_seq_len: int = 2048
230
+
231
+ # ── Learning Rate ─────────────────────────────────────────────────────────
232
+ lr_max: float = 3e-4
233
+ lr_min: float = 3e-5
234
+ lr_warmup_tokens: int = 500_000_000 # 500M tokens warmup directly in Phase 2
235
+ lr_schedule: str = "cosine"
236
+
237
+ # ── AdamW ─────────────────────────────────────────────────────────────────
238
+ beta1: float = 0.9
239
+ beta2: float = 0.95
240
+ eps: float = 1e-8
241
+ weight_decay: float = 0.1
242
+ grad_clip: float = 1.0
243
+
244
+ # ── Parameter Groups LR Multipliers ──────────────────────────────────────
245
+ embedding_lr_mult: float = 0.5
246
+ ssm_lr_mult: float = 0.3
247
+ router_lr_mult: float = 0.5
248
+
249
+ # ── Checkpointing (Hyper-Resilient: saves every 20-30 mins OR 500M tokens)
250
+ save_every_n_tokens: int = 500_000_000 # checkpoint every 500M tokens (~8 mins on 32x GPUs)
251
+ save_every_minutes: float = 20.0 # time-based checkpoint guard (every 20 minutes)
252
+ keep_last_n_checkpoints: int = 10 # keep 10 historical snapshots
253
+
254
+ # ── Logging ───────────────────────────────────────────────────────────────
255
+ log_every_n_steps: int = 1 # log every step for full metrics visibility
256
+ eval_every_n_steps: int = 200 # eval perplexity every 200 steps
257
+ wandb_project: str = "forge_500m_pretrain"
258
+ wandb_entity: Optional[str] = None
259
+
260
+ # ── GPU/Distributed ───────────���───────────────────────────────────────────
261
+ num_gpus: int = 1 # default 1x H100; set to 16 for 16x
262
+ bf16: bool = True
263
+ fp8: bool = False # H100 FP8 (experimental)
264
+ deepspeed_config: str = "./configs/ds_zero3.json"
265
+ torch_compile: bool = True
266
+ compile_mode: str = "max-autotune" # peak throughput
267
+
268
+ # ── Data ──────────────────────────────────────────────────────────────────
269
+ num_dataloader_workers: int = 4
270
+ prefetch_factor: int = 4
271
+ seed: int = 42
272
+
273
+ @property
274
+ def gradient_accumulation_steps_phase2(self) -> int:
275
+ """Accumulation steps for phase 2 global batch."""
276
+ tokens_per_gpu_step = self.micro_batch_size_per_gpu * self.phase2_seq_len
277
+ tokens_per_step_all_gpus = tokens_per_gpu_step * self.num_gpus
278
+ return self.phase2_global_batch_tokens // tokens_per_step_all_gpus
279
+
280
+
281
+ @dataclass
282
+ class SFTConfig:
283
+ """Supervised Fine-Tuning configuration for FORGE-500M."""
284
+
285
+ base_model_path: str = "./checkpoints/forge_500m_pretrain/final"
286
+ output_dir: str = "./checkpoints/forge_500m_sft"
287
+ data_dir: str = "Phase-Technologies/forge-3b-sft-data"
288
+
289
+ # ── Training ──────────────────────────────────────────────────────────────
290
+ total_tokens: int = 1_400_000_000
291
+ global_batch_tokens: int = 524_288 # 512K per step
292
+ micro_batch_size_per_gpu: int = 4
293
+ seq_len: int = 4096
294
+
295
+ # ── Optimizer ─────────────────────────────────────────────────────────────
296
+ lr_max: float = 1e-5
297
+ lr_min: float = 1e-6
298
+ lr_schedule: str = "cosine"
299
+ beta1: float = 0.9
300
+ beta2: float = 0.95
301
+ eps: float = 1e-8
302
+ weight_decay: float = 0.0
303
+ grad_clip: float = 0.5
304
+
305
+ epochs: int = 1
306
+ loss_on_prompt: bool = False # only compute loss on assistant turns
307
+
308
+ save_every_n_steps: int = 100
309
+ wandb_project: str = "forge_500m_sft"
310
+ deepspeed_config: str = "./configs/ds_zero3_sft.json"
311
+ num_gpus: int = 1
312
+ bf16: bool = True
313
+ torch_compile: bool = True
314
+ compile_mode: str = "max-autotune"
315
+
316
+
317
+ @dataclass
318
+ class DPOConfig:
319
+ """Direct Preference Optimization configuration for FORGE-500M."""
320
+
321
+ base_model_path: str = "./checkpoints/forge_500m_sft/final"
322
+ output_dir: str = "./checkpoints/forge_500m_dpo"
323
+ data_dir: str = "Phase-Technologies/forge-3b-dpo-data"
324
+
325
+ # ── DPO ───────────────────────────────────────────────────────────────────
326
+ beta: float = 0.1 # KL penalty coefficient
327
+ loss_type: str = "dpo" # "dpo" | "ipo" | "cdpo"
328
+ reference_free: bool = False
329
+
330
+ # ── Training ──────────────────────────────────────────────────────────────
331
+ n_preference_pairs: int = 200_000
332
+ batch_size_pairs: int = 8 # preference pairs per GPU step
333
+ gradient_accumulation_steps: int = 4
334
+ seq_len: int = 4096
335
+
336
+ # ── Optimizer ─────────────────────────────────────────────────────────────
337
+ lr: float = 5e-7
338
+ beta1: float = 0.9
339
+ beta2: float = 0.99
340
+ eps: float = 1e-8
341
+ weight_decay: float = 0.0
342
+ grad_clip: float = 0.3
343
+ n_epochs: int = 1
344
+
345
+ save_every_n_steps: int = 100
346
+ wandb_project: str = "forge_500m_dpo"
347
+ deepspeed_config: str = "./configs/ds_zero3_sft.json"
348
+ num_gpus: int = 1
349
+ bf16: bool = True
350
+ torch_compile: bool = True
351
+ compile_mode: str = "max-autotune"
352
+
353
+
354
+ # ─────────────────────────────────────────────────────────────────────────────
355
+ # TOKENIZER CONFIGURATION
356
+ # ─────────────────────────────────────────────────────────────────────────────
357
+
358
+ @dataclass
359
+ class CrayonConfig:
360
+ """CRAYON tokenizer configuration."""
361
+ profile: str = "standard" # "standard" (206k) | "lite" (50k)
362
+ device: str = "cpu" # "cpu" is faster for tokenization (23M vs 1.2M tok/s)
363
+ pad_token: str = "<|pad|>"
364
+ bos_token: str = "<|bos|>"
365
+ eos_token: str = "<|eos|>"
366
+ sep_token: str = "<|sep|>"
367
+ sys_token: str = "<|sys|>"
368
+ usr_token: str = "<|usr|>"
369
+ asst_token: str = "<|asst|>"
370
+ tool_token: str = "<|tool|>"
371
+ tool_resp_token: str = "<|tool_resp|>"
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b503a6ee2aac8f9b57040d5daf35f30d0ac126fb2525686c67bb3da556dbb1ff
3
+ size 1422282392
model/__init__.py ADDED
File without changes
model/arg_layer.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ARG (Adaptive Recurrent Gating) Layer β€” FORGE's core sequence mixer.
3
+
4
+ Combines:
5
+ 1. Complex-eigenvalue selective SSM (recurrent branch)
6
+ 2. Local windowed GQA (local attention branch)
7
+ 3. Learned scalar gate α_t ∈ [0,1] that blends the two
8
+ 4. Compound Positional Bias (CPB) for SSM initialization
9
+ """
10
+
11
+ from __future__ import annotations
12
+ import math
13
+ from typing import Optional, Tuple
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+ from einops import rearrange
19
+
20
+ from .dgn_norm import build_norm
21
+ from .rotary_embedding import RotaryEmbedding
22
+ from .triton_kernels import fused_swiglu
23
+ from .triton_ssm import fused_triton_ssm
24
+
25
+
26
+
27
+ class ARGLayer(nn.Module):
28
+ """
29
+ Adaptive Recurrent Gating Layer.
30
+
31
+ For each token t:
32
+ h_recur_t = ComplexSSM(x_{0:t})
33
+ h_local_t = WindowedGQA(x_{t-W:t})
34
+ Ξ±_t = Οƒ(w_g Β· x_t)
35
+ output_t = Ξ±_t * h_local_t + (1 - Ξ±_t) * h_recur_t
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ d_model: int = 1280,
41
+ d_inner: int = 1280,
42
+ d_state: int = 48,
43
+ d_rank: int = 48,
44
+ conv_kernel: int = 4,
45
+ local_window: int = 64,
46
+ local_n_heads: int = 8,
47
+ local_n_kv_heads: int = 2,
48
+ head_dim: int = 80,
49
+ norm_type: str = "dgn",
50
+ dgn_n_groups: int = 16,
51
+ norm_eps: float = 1e-6,
52
+ use_flash_attention: bool = True,
53
+ rope_base: float = 500_000.0,
54
+ layer_idx: int = 0,
55
+ max_seq_len: int = 4096,
56
+ ):
57
+ super().__init__()
58
+ self.d_model = d_model
59
+ self.d_inner = d_inner
60
+ self.d_state = d_state
61
+ self.d_rank = d_rank
62
+ self.conv_kernel = conv_kernel
63
+ self.local_window = local_window
64
+ self.local_n_heads = local_n_heads
65
+ self.local_n_kv_heads = local_n_kv_heads
66
+ self.head_dim = head_dim
67
+ self.use_flash_attention = use_flash_attention
68
+ self.layer_idx = layer_idx
69
+
70
+ # ── Pre-norm ──────────────────────────────────────────────────────────
71
+ self.norm = build_norm(norm_type, d_model, dgn_n_groups, norm_eps)
72
+
73
+ # ── Recurrent Branch ──────────────────────────────────────────────────
74
+ # Input projection: x β†’ (x_inner, z) both d_inner
75
+ self.in_proj = nn.Linear(d_model, 2 * d_inner, bias=False)
76
+
77
+ # Depthwise causal conv for local mixing
78
+ self.conv1d = nn.Conv1d(
79
+ d_inner, d_inner,
80
+ kernel_size=conv_kernel,
81
+ padding=conv_kernel - 1, # causal padding
82
+ groups=d_inner,
83
+ bias=True,
84
+ )
85
+
86
+ # Input-dependent SSM parameters (low-rank for Ξ”)
87
+ self.x_proj = nn.Linear(d_inner, d_rank + 2 * d_state, bias=False)
88
+ self.dt_proj = nn.Linear(d_rank, d_inner, bias=True)
89
+
90
+ # Complex SSM eigenvalue parameters
91
+ # Uses real-pair encoding: each complex eigenvalue Ξ» = -exp(Ξ½) Β± jΞΈ
92
+ # is represented as two real state channels, so d_state must be even.
93
+ # theta (oscillation) is implicitly learned through B/C projections.
94
+ assert d_state % 2 == 0, "d_state must be even for complex-pair encoding"
95
+ self.d_state_complex = d_state // 2
96
+
97
+ self.nu = nn.Parameter(torch.zeros(self.d_state_complex)) # decay (log scale)
98
+
99
+ # Skip connection coefficient D
100
+ self.D = nn.Parameter(torch.ones(d_inner))
101
+
102
+ # Output projection
103
+ self.out_proj = nn.Linear(d_inner, d_model, bias=False)
104
+
105
+ # ── Local Attention Branch ────────────────────────────────────────────
106
+ q_dim = local_n_heads * head_dim
107
+ kv_dim = local_n_kv_heads * head_dim
108
+
109
+ self.local_q = nn.Linear(d_model, q_dim, bias=False)
110
+ self.local_k = nn.Linear(d_model, kv_dim, bias=False)
111
+ self.local_v = nn.Linear(d_model, kv_dim, bias=False)
112
+ self.local_o = nn.Linear(q_dim, d_model, bias=False)
113
+
114
+ # Local RoPE (initialize with safe maximum length of 8192 to avoid dynamic cache expansion during forward pass)
115
+ self.local_rope = RotaryEmbedding(
116
+ head_dim=head_dim,
117
+ max_seq_len=max_seq_len * 2,
118
+ base=rope_base,
119
+ )
120
+
121
+ # ── Adaptive Gate ─────────────────────────────────────────────────────
122
+ self.gate_proj = nn.Linear(d_model, 1, bias=True)
123
+ nn.init.zeros_(self.gate_proj.weight)
124
+ nn.init.zeros_(self.gate_proj.bias) # Ξ±=0.5 at init: sigmoid(0) = 0.5
125
+
126
+ # Initialize weights
127
+ self._init_weights()
128
+
129
+ def _init_weights(self):
130
+ """Careful initialization for stable training."""
131
+ # dt_proj bias: positive to encourage non-trivial step sizes at init
132
+ dt_init_std = self.d_rank ** -0.5
133
+ nn.init.uniform_(self.dt_proj.weight, -dt_init_std, dt_init_std)
134
+ dt_bias = torch.exp(
135
+ torch.rand(self.d_inner) * (math.log(0.1) - math.log(0.001)) + math.log(0.001)
136
+ )
137
+ with torch.no_grad():
138
+ self.dt_proj.bias.copy_(dt_bias + torch.log(-torch.expm1(-dt_bias)))
139
+
140
+ # nu init β€” zero β†’ moderate decay at start
141
+ nn.init.zeros_(self.nu)
142
+
143
+ # Conv1d β€” identity-like init
144
+ nn.init.zeros_(self.conv1d.weight)
145
+ for i in range(self.d_inner):
146
+ self.conv1d.weight.data[i, 0, -1] = 1.0
147
+
148
+ # ─────────────────────────────────────────────────────────────────────────
149
+ # COMPLEX EIGENVALUE β†’ REAL A MATRIX
150
+ # ─────────────────────────────────────────────────────────────────────────
151
+
152
+ def _build_real_A(self) -> torch.Tensor:
153
+ """
154
+ Build the (d_inner, d_state) real-valued A matrix for selective_scan_fn.
155
+
156
+ Each complex eigenvalue Ξ»_i = -exp(Ξ½_i) + jΞΈ_i is encoded as two real
157
+ state channels with the same decay rate. The theta (oscillation) enters
158
+ via the B/C projections. mamba_ssm's selective_scan_fn discretizes A
159
+ internally via exp(A * Ξ”), so we pass the continuous-time log-decay
160
+ (negative real, per state channel).
161
+ """
162
+ decay = -F.softplus(self.nu.float()) # fp32 β€” mamba_ssm requires A in fp32
163
+ A_diag = decay.repeat_interleave(2) # (d_state,)
164
+ A = A_diag.unsqueeze(0).expand(self.d_inner, -1).contiguous().float() # (d_inner, d_state), fp32
165
+ return A
166
+
167
+ # ─────────────────────────────────────────────────────────────────────────
168
+ # RECURRENT BRANCH
169
+ # ─────────────────────────────────────────────────────────────────────────
170
+
171
+ @torch._dynamo.disable
172
+ def recurrent_branch(
173
+ self,
174
+ x: torch.Tensor, # (B, T, d_model)
175
+ position_offset: int = 0,
176
+ ) -> torch.Tensor:
177
+ """
178
+ Selective SSM recurrent branch using mamba_ssm's fused CUDA kernel.
179
+ Returns (B, T, d_model).
180
+
181
+ Uses selective_scan_fn (same kernel as Mamba) which:
182
+ - Processes all channels in parallel with an efficient associative scan
183
+ - Applies delta_softplus internally (no manual softplus on dt)
184
+ - Fuses the z-gate SiLU multiplication inside the kernel
185
+ - Eliminates 512Γ— Python-level kernel launches per layer
186
+ """
187
+ B, T, _ = x.shape
188
+
189
+ # ── Project inputs ────────────────────────────────────────────────────
190
+ xz = self.in_proj(x) # (B, T, 2*d_inner)
191
+ x_inner, z = xz.chunk(2, dim=-1) # each (B, T, d_inner)
192
+
193
+ # ── Causal conv for local mixing ──────────────────────────────────────
194
+ x_conv = rearrange(x_inner, 'b t d -> b d t')
195
+ x_conv = self.conv1d(x_conv)[:, :, :T] # trim causal padding, still (B, d_inner, T)
196
+ x_conv = F.silu(x_conv) # (B, d_inner, T)
197
+ x_conv_t = rearrange(x_conv, 'b d t -> b t d')
198
+
199
+ # ── Input-dependent SSM parameters ────────────────────────────────────
200
+ x_dbl = self.x_proj(x_conv_t) # (B, T, d_rank + 2*d_state)
201
+ dt_raw, B_ssm, C_ssm = x_dbl.split([self.d_rank, self.d_state, self.d_state], dim=-1)
202
+
203
+ # dt: per-channel step size β€” (B, T, d_inner) β†’ (B, d_inner, T)
204
+ dt = F.softplus(self.dt_proj(dt_raw))
205
+ dt = rearrange(dt, 'b t d -> b d t').contiguous()
206
+
207
+ # B, C: (B, d_state, T)
208
+ B_ssm = rearrange(B_ssm, 'b t n -> b n t').contiguous()
209
+ C_ssm = rearrange(C_ssm, 'b t n -> b n t').contiguous()
210
+
211
+ # ── Build A from complex eigenvalue params ────────────────────────────
212
+ A = self._build_real_A() # (d_inner, d_state), negative real
213
+
214
+ # ── Pure Triton Fused Selective SSM Scan ──────────────────────────────
215
+ z_t = rearrange(z, 'b t d -> b d t').contiguous()
216
+ y = fused_triton_ssm(x_conv, dt, A, B_ssm, C_ssm, self.D, z_t)
217
+ y = rearrange(y, 'b d t -> b t d')
218
+
219
+ return self.out_proj(y.to(dtype=x.dtype)) # (B, T, d_model)
220
+
221
+ # ─────────────────────────────────────────────────────────────────────────
222
+ # LOCAL ATTENTION BRANCH
223
+ # ─────────────────────────────────────────────────────────────────────────
224
+
225
+ @torch._dynamo.disable
226
+ def local_attention_branch(
227
+ self,
228
+ x: torch.Tensor, # (B, T, d_model)
229
+ ) -> torch.Tensor:
230
+ """
231
+ Windowed GQA with window size W.
232
+ Uses native PyTorch SDPA with sliding window mask.
233
+ Returns (B, T, d_model).
234
+ """
235
+ B, T, _ = x.shape
236
+ W = self.local_window
237
+
238
+ Q = self.local_q(x) # (B, T, n_heads * head_dim)
239
+ K = self.local_k(x) # (B, T, n_kv_heads * head_dim)
240
+ V = self.local_v(x)
241
+
242
+ Q = rearrange(Q, 'b t (h d) -> b h t d', d=self.head_dim) # (B, H, T, D)
243
+ K = rearrange(K, 'b t (h d) -> b h t d', d=self.head_dim)
244
+ V = rearrange(V, 'b t (h d) -> b h t d', d=self.head_dim)
245
+
246
+ # Apply local RoPE
247
+ Q, K = self.local_rope.apply_rotary(Q, K)
248
+
249
+ # GQA: expand KV to match Q heads
250
+ n_rep = self.local_n_heads // self.local_n_kv_heads
251
+ if n_rep > 1:
252
+ K = K.repeat_interleave(n_rep, dim=1) # (B, H, T, D)
253
+ V = V.repeat_interleave(n_rep, dim=1)
254
+
255
+ # Fast PyTorch SDPA with sliding window mask
256
+ indices = torch.arange(T, device=x.device)
257
+ mask_2d = (indices.unsqueeze(0) <= indices.unsqueeze(1)) & \
258
+ (indices.unsqueeze(0) >= (indices.unsqueeze(1) - W))
259
+ mask_4d = mask_2d.unsqueeze(0).unsqueeze(0)
260
+ attn_out = F.scaled_dot_product_attention(Q, K, V, attn_mask=mask_4d)
261
+ out = rearrange(attn_out, 'b h t d -> b t (h d)')
262
+
263
+ return self.local_o(out) # (B, T, d_model)
264
+
265
+ # ─────────────────────────────────────────────────────────────────────────
266
+ # FORWARD PASS
267
+ # ─────────────────────────────────────────────────────────────────────────
268
+
269
+ def forward(
270
+ self,
271
+ x: torch.Tensor,
272
+ position_offset: int = 0,
273
+ ) -> torch.Tensor:
274
+ """
275
+ x: (B, T, d_model)
276
+ Returns: (B, T, d_model) β€” residual connection applied inside
277
+ """
278
+ residual = x
279
+ x_norm = self.norm(x)
280
+
281
+ # Two branches
282
+ h_recur = self.recurrent_branch(x_norm, position_offset)
283
+ h_local = self.local_attention_branch(x_norm)
284
+
285
+ # Learned gate
286
+ alpha = torch.sigmoid(self.gate_proj(x_norm)) # (B, T, 1)
287
+
288
+ # Blend and add residual
289
+ mixed = alpha * h_local + (1.0 - alpha) * h_recur # (B, T, d_model)
290
+
291
+ return residual + mixed
model/dgn_norm.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Differential Group Normalization layer."""
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ from .triton_kernels import fused_dgn, TRITON_AVAILABLE
6
+
7
+
8
+ class DifferentialGroupNorm(nn.Module):
9
+ """
10
+ Differential Group Normalization (DGN).
11
+
12
+ Divides d_model features into G groups and applies independent
13
+ RMS normalization with learned scale+bias to each group.
14
+
15
+ Parameters: 2 * d_model (weight + bias) vs RMSNorm's 1 * d_model.
16
+ The extra bias (absent in RMSNorm) provides an additive degree of freedom
17
+ per group, shown empirically to speed up early training convergence by ~15%.
18
+ """
19
+
20
+ def __init__(self, d_model: int, n_groups: int = 16, eps: float = 1e-6):
21
+ super().__init__()
22
+ assert d_model % n_groups == 0, \
23
+ f"d_model={d_model} must be divisible by n_groups={n_groups}"
24
+
25
+ self.d_model = d_model
26
+ self.n_groups = n_groups
27
+ self.group_size = d_model // n_groups
28
+ self.eps = eps
29
+
30
+ # Learnable parameters β€” initialized to identity transform
31
+ self.weight = nn.Parameter(torch.ones(d_model))
32
+ self.bias = nn.Parameter(torch.zeros(d_model))
33
+
34
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
35
+ """
36
+ x: (..., d_model)
37
+ Returns: (..., d_model), normalized within each group.
38
+ """
39
+ return fused_dgn(x, self.weight, self.bias, self.n_groups, self.eps)
40
+
41
+ def extra_repr(self) -> str:
42
+ return f"d_model={self.d_model}, n_groups={self.n_groups}, eps={self.eps}"
43
+
44
+
45
+ class RMSNorm(nn.Module):
46
+ """Standard RMSNorm β€” used as fallback or when DGN is disabled."""
47
+
48
+ def __init__(self, d_model: int, eps: float = 1e-6):
49
+ super().__init__()
50
+ self.weight = nn.Parameter(torch.ones(d_model))
51
+ self.eps = eps
52
+
53
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
54
+ norm = x.float().pow(2).mean(-1, keepdim=True).add(self.eps).sqrt()
55
+ return (x.float() / norm * self.weight).to(x.dtype)
56
+
57
+
58
+ def build_norm(norm_type: str, d_model: int, n_groups: int = 16,
59
+ eps: float = 1e-6) -> nn.Module:
60
+ if norm_type == "dgn":
61
+ return DifferentialGroupNorm(d_model, n_groups, eps)
62
+ elif norm_type == "rmsnorm":
63
+ return RMSNorm(d_model, eps)
64
+ else:
65
+ raise ValueError(f"Unknown norm_type: {norm_type}")
model/forge_model.py ADDED
@@ -0,0 +1,558 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORGE-3B: Complete Model Architecture
3
+ Integrates ARG, MHA, Dense FFN, HSE FFN into the full autoregressive LM.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import math
8
+ import logging
9
+ from typing import Optional, List, Tuple, Dict, Any, Union
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+ from torch.utils.checkpoint import checkpoint
15
+
16
+ from .arg_layer import ARGLayer
17
+ from .mha_layer import GlobalMHALayer
18
+ from .hse_layer import HSELayer, DenseSwiGLUFFN
19
+ from .dgn_norm import build_norm
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ForgeBlock(nn.Module):
25
+ """
26
+ One FORGE block: SeqMixer + FFN (both with pre-norm and residual).
27
+
28
+ Blocks are composed sequentially. The seq_mixer is either ARG or MHA;
29
+ the ffn is either DenseSwiGLUFFN or HSELayer β€” determined by layer_idx.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ d_model: int,
35
+ layer_idx: int,
36
+ is_mha: bool,
37
+ is_hse: bool,
38
+ # ARG config
39
+ arg_d_inner: int = 1280,
40
+ arg_d_state: int = 48,
41
+ arg_d_rank: int = 48,
42
+ arg_conv_kernel: int = 4,
43
+ arg_local_window: int = 64,
44
+ arg_local_n_heads: int = 8,
45
+ arg_local_n_kv_heads: int = 2,
46
+ arg_head_dim: int = 80,
47
+ # MHA config
48
+ mha_n_heads: int = 16,
49
+ mha_n_kv_heads: int = 4,
50
+ mha_head_dim: int = 80,
51
+ rope_base: float = 500_000.0,
52
+ rope_scaling_type: Optional[str] = None,
53
+ rope_scaling_factor: float = 1.0,
54
+ max_seq_len: int = 4096,
55
+ # Dense FFN config
56
+ dense_d_ff: int = 3200,
57
+ # HSE config
58
+ hse_n_domains: int = 4,
59
+ hse_n_experts_per_domain: int = 8,
60
+ hse_top_k: int = 2,
61
+ hse_d_ff_expert: int = 288,
62
+ hse_capacity_factor: float = 1.25,
63
+ hse_expert_dropout: float = 0.1,
64
+ hse_aux_loss_alpha: float = 0.01,
65
+ # Norm
66
+ norm_type: str = "dgn",
67
+ dgn_n_groups: int = 16,
68
+ norm_eps: float = 1e-6,
69
+ use_flash_attention: bool = True,
70
+ dropout: float = 0.0,
71
+ ):
72
+ super().__init__()
73
+ self.layer_idx = layer_idx
74
+ self.is_mha = is_mha
75
+ self.is_hse = is_hse
76
+
77
+ # ── Sequence Mixer ────────────────────────────────────────────────────
78
+ if is_mha:
79
+ self.seq_mixer = GlobalMHALayer(
80
+ d_model=d_model,
81
+ n_heads=mha_n_heads,
82
+ n_kv_heads=mha_n_kv_heads,
83
+ head_dim=mha_head_dim,
84
+ rope_base=rope_base,
85
+ rope_scaling_type=rope_scaling_type,
86
+ rope_scaling_factor=rope_scaling_factor,
87
+ max_seq_len=max_seq_len,
88
+ dropout=dropout,
89
+ norm_type=norm_type,
90
+ dgn_n_groups=dgn_n_groups,
91
+ norm_eps=norm_eps,
92
+ use_flash_attention=use_flash_attention,
93
+ layer_idx=layer_idx,
94
+ )
95
+ else:
96
+ self.seq_mixer = ARGLayer(
97
+ d_model=d_model,
98
+ d_inner=arg_d_inner,
99
+ d_state=arg_d_state,
100
+ d_rank=arg_d_rank,
101
+ conv_kernel=arg_conv_kernel,
102
+ local_window=arg_local_window,
103
+ local_n_heads=arg_local_n_heads,
104
+ local_n_kv_heads=arg_local_n_kv_heads,
105
+ head_dim=arg_head_dim,
106
+ norm_type=norm_type,
107
+ dgn_n_groups=dgn_n_groups,
108
+ norm_eps=norm_eps,
109
+ use_flash_attention=use_flash_attention,
110
+ rope_base=rope_base,
111
+ layer_idx=layer_idx,
112
+ max_seq_len=max_seq_len,
113
+ )
114
+
115
+ # ── FFN ───────────────────────────────────────────────────────────────
116
+ if is_hse:
117
+ self.ffn = HSELayer(
118
+ d_model=d_model,
119
+ n_domains=hse_n_domains,
120
+ n_experts_per_domain=hse_n_experts_per_domain,
121
+ top_k=hse_top_k,
122
+ d_ff_expert=hse_d_ff_expert,
123
+ capacity_factor=hse_capacity_factor,
124
+ expert_dropout=hse_expert_dropout,
125
+ aux_loss_alpha=hse_aux_loss_alpha,
126
+ norm_type=norm_type,
127
+ dgn_n_groups=dgn_n_groups,
128
+ norm_eps=norm_eps,
129
+ layer_idx=layer_idx,
130
+ )
131
+ else:
132
+ self.ffn = DenseSwiGLUFFN(
133
+ d_model=d_model,
134
+ d_ff=dense_d_ff,
135
+ norm_type=norm_type,
136
+ dgn_n_groups=dgn_n_groups,
137
+ norm_eps=norm_eps,
138
+ )
139
+
140
+ def forward(
141
+ self,
142
+ x: torch.Tensor,
143
+ attention_mask: Optional[torch.Tensor] = None,
144
+ position_ids: Optional[torch.Tensor] = None,
145
+ position_offset: int = 0,
146
+ use_cache: bool = False,
147
+ return_aux_loss: bool = True,
148
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
149
+
150
+ aux_loss = None
151
+
152
+ # ── Sequence Mixing ───────────────────────────────────────────────────
153
+ if self.is_mha:
154
+ x, _ = self.seq_mixer(
155
+ x, attention_mask=attention_mask,
156
+ position_ids=position_ids, use_cache=use_cache
157
+ )
158
+ else:
159
+ x = self.seq_mixer(x, position_offset=position_offset)
160
+
161
+ # ── FFN ───────────────────────────────────────────────────────────────
162
+ if self.is_hse:
163
+ x, aux_loss = self.ffn(x, return_aux_loss=return_aux_loss)
164
+ else:
165
+ x = self.ffn(x)
166
+
167
+ return x, aux_loss
168
+
169
+
170
+ class ForgeModel(nn.Module):
171
+ """
172
+ FORGE-3B: Full autoregressive language model.
173
+
174
+ Architecture:
175
+ - Token embedding (tied to LM head)
176
+ - 36 ForgeBlocks (27 ARG + 9 MHA for seq mixing; 18 Dense + 18 HSE for FFN)
177
+ - Final DGN normalization
178
+ - LM head (weight-tied to embedding)
179
+ """
180
+
181
+ def __init__(self, config):
182
+ super().__init__()
183
+ self.config = config
184
+
185
+ d = config.d_model
186
+
187
+ # ── Token Embedding ───────────────────────────────────────────────────
188
+ self.embed_tokens = nn.Embedding(config.vocab_size, d, padding_idx=0)
189
+
190
+ # ── FORGE Blocks ──────────────────────────────────────────────────────
191
+ mha_set = set(config.mha_layer_indices)
192
+ hse_set = set(config.hse_ffn_layer_indices)
193
+
194
+ self.layers = nn.ModuleList([
195
+ ForgeBlock(
196
+ d_model=d,
197
+ layer_idx=i,
198
+ is_mha=(i in mha_set),
199
+ is_hse=(i in hse_set),
200
+ arg_d_inner=config.arg_d_inner,
201
+ arg_d_state=config.arg_d_state,
202
+ arg_d_rank=config.arg_d_rank,
203
+ arg_conv_kernel=config.arg_conv_kernel,
204
+ arg_local_window=config.arg_local_window,
205
+ arg_local_n_heads=config.arg_local_n_heads,
206
+ arg_local_n_kv_heads=config.arg_local_n_kv_heads,
207
+ arg_head_dim=config.arg_head_dim,
208
+ mha_n_heads=config.mha_n_heads,
209
+ mha_n_kv_heads=config.mha_n_kv_heads,
210
+ mha_head_dim=config.mha_head_dim,
211
+ rope_base=config.rope_theta,
212
+ rope_scaling_type=config.rope_scaling_type,
213
+ rope_scaling_factor=config.rope_scaling_factor,
214
+ max_seq_len=config.max_seq_len,
215
+ dense_d_ff=config.dense_d_ff,
216
+ hse_n_domains=config.hse_n_domains,
217
+ hse_n_experts_per_domain=config.hse_n_experts_per_domain,
218
+ hse_top_k=config.hse_top_k,
219
+ hse_d_ff_expert=config.hse_d_ff_expert,
220
+ hse_capacity_factor=config.hse_capacity_factor,
221
+ hse_expert_dropout=config.hse_expert_dropout,
222
+ hse_aux_loss_alpha=config.hse_aux_loss_alpha,
223
+ norm_type=config.norm_type,
224
+ dgn_n_groups=config.dgn_n_groups,
225
+ norm_eps=config.norm_eps,
226
+ use_flash_attention=config.use_flash_attention,
227
+ )
228
+ for i in range(config.n_layers)
229
+ ])
230
+
231
+ # ── Final Norm ────────────────────────────────────────────────────────
232
+ self.norm_final = build_norm(config.norm_type, d, config.dgn_n_groups, config.norm_eps)
233
+
234
+ # ── LM Head ───────────────────────────────────────────────────────────
235
+ self.lm_head = nn.Linear(d, config.vocab_size, bias=False)
236
+
237
+ # Tie weights
238
+ if config.tie_word_embeddings:
239
+ self.lm_head.weight = self.embed_tokens.weight
240
+
241
+ # ── Gradient Checkpointing Configuration ─────────────────────────────
242
+ self.gradient_checkpointing_enabled = config.use_gradient_checkpointing
243
+ self.gradient_checkpointing_ratio = config.gradient_checkpointing_ratio
244
+ # Checkpoint every N-th layer
245
+ if config.gradient_checkpointing_ratio > 0:
246
+ self._checkpoint_interval = max(1, int(1.0 / config.gradient_checkpointing_ratio))
247
+ else:
248
+ self._checkpoint_interval = 1
249
+
250
+ # Apply weight initialization
251
+ self._init_weights()
252
+
253
+ # Log parameter count
254
+ n_params = sum(p.ds_numel if hasattr(p, "ds_numel") else p.numel() for p in self.parameters())
255
+ logger.info(f"ForgeModel: {n_params/1e9:.3f}B total parameters")
256
+ emb_p = self.embed_tokens.weight
257
+ emb_numel = emb_p.ds_numel if hasattr(emb_p, "ds_numel") else emb_p.numel()
258
+ logger.info(f" Embedding: {emb_numel/1e6:.1f}M")
259
+ logger.info(f" N layers: {config.n_layers} "
260
+ f"({len(mha_set)} MHA + {config.n_layers - len(mha_set)} ARG, "
261
+ f"{len(hse_set)} HSE + {config.n_layers - len(hse_set)} Dense)")
262
+
263
+ def _init_weights(self):
264
+ """Initialize all model weights with scaled normal distribution."""
265
+ n_layers = self.config.n_layers
266
+
267
+ for module in self.modules():
268
+ if isinstance(module, nn.Linear):
269
+ nn.init.normal_(module.weight, std=self.config.init_std)
270
+ if module.bias is not None:
271
+ nn.init.zeros_(module.bias)
272
+ elif isinstance(module, nn.Embedding):
273
+ nn.init.normal_(module.weight, std=self.config.init_std)
274
+ if module.padding_idx is not None:
275
+ module.weight.data[module.padding_idx].zero_()
276
+
277
+ # Residual scaling: scale output projections by 1/sqrt(2*n_layers)
278
+ # This prevents the residual stream from growing with depth
279
+ if self.config.init_std_residual_scale:
280
+ scale = 1.0 / math.sqrt(2 * n_layers)
281
+ for layer in self.layers:
282
+ if isinstance(layer.seq_mixer, ARGLayer):
283
+ nn.init.normal_(layer.seq_mixer.out_proj.weight, std=scale)
284
+ nn.init.normal_(layer.seq_mixer.local_o.weight, std=scale)
285
+ elif isinstance(layer.seq_mixer, GlobalMHALayer):
286
+ nn.init.normal_(layer.seq_mixer.o_proj.weight, std=scale)
287
+
288
+ if isinstance(layer.ffn, DenseSwiGLUFFN):
289
+ nn.init.normal_(layer.ffn.down.weight, std=scale)
290
+ elif isinstance(layer.ffn, HSELayer):
291
+ for expert in layer.ffn.experts:
292
+ nn.init.normal_(expert.down.weight, std=scale)
293
+
294
+ def get_input_embeddings(self) -> nn.Embedding:
295
+ return self.embed_tokens
296
+
297
+ def set_input_embeddings(self, value: nn.Embedding):
298
+ self.embed_tokens = value
299
+
300
+ def enable_gradient_checkpointing(self):
301
+ self.gradient_checkpointing_enabled = True
302
+ logger.info("Gradient checkpointing enabled")
303
+
304
+ def disable_gradient_checkpointing(self):
305
+ self.gradient_checkpointing_enabled = False
306
+
307
+ def forward(
308
+ self,
309
+ input_ids: torch.Tensor, # (B, T)
310
+ attention_mask: Optional[torch.Tensor] = None,
311
+ position_ids: Optional[torch.Tensor] = None,
312
+ labels: Optional[torch.Tensor] = None, # (B, T) for LM loss
313
+ use_cache: bool = False,
314
+ return_aux_loss: bool = True,
315
+ output_hidden_states: bool = False,
316
+ ) -> Dict[str, torch.Tensor]:
317
+ """
318
+ Forward pass for FORGE-3B.
319
+
320
+ Returns dict with:
321
+ 'loss': cross-entropy + aux_loss (if labels provided)
322
+ 'logits': (B, T, vocab_size)
323
+ 'aux_loss': sum of MoE load-balance losses
324
+ 'hidden_states': list of layer outputs (if output_hidden_states)
325
+ """
326
+ B, T = input_ids.shape
327
+ device = input_ids.device
328
+
329
+ # ── Token Embeddings ──────────────────────────────────────────────────
330
+ h = self.embed_tokens(input_ids) # (B, T, d_model)
331
+
332
+ if position_ids is None:
333
+ position_ids = torch.arange(T, device=device).unsqueeze(0).expand(B, -1)
334
+
335
+ # ── Forward Through Layers ────────────────────────────────────────────
336
+ all_hidden_states = [] if output_hidden_states else None
337
+ total_aux_loss = torch.zeros(1, device=device, dtype=h.dtype)
338
+ n_hse_layers = 0
339
+
340
+ for layer_idx, layer in enumerate(self.layers):
341
+ if output_hidden_states:
342
+ all_hidden_states.append(h)
343
+
344
+ # Selective gradient checkpointing
345
+ if (self.gradient_checkpointing_enabled and
346
+ self.training and
347
+ layer_idx % self._checkpoint_interval == 0):
348
+
349
+ def _forward_layer(h_, layer_=layer):
350
+ out, aux = layer_(
351
+ h_,
352
+ attention_mask=attention_mask,
353
+ position_ids=position_ids,
354
+ position_offset=0,
355
+ use_cache=False,
356
+ return_aux_loss=True,
357
+ )
358
+ return out, aux if aux is not None else torch.zeros(1, device=h_.device)
359
+
360
+ h, aux = checkpoint(
361
+ _forward_layer, h,
362
+ use_reentrant=False, # required for PyTorch DDP to prevent double-marking ready
363
+ preserve_rng_state=True,
364
+ )
365
+ else:
366
+ h, aux = layer(
367
+ h,
368
+ attention_mask=attention_mask,
369
+ position_ids=position_ids,
370
+ position_offset=0,
371
+ use_cache=use_cache,
372
+ return_aux_loss=return_aux_loss,
373
+ )
374
+
375
+ if aux is not None and return_aux_loss:
376
+ total_aux_loss = total_aux_loss + aux
377
+ n_hse_layers += 1
378
+
379
+ # ── Final Norm ────────────────────────────────────────────────────────
380
+ h = self.norm_final(h) # (B, T, d_model)
381
+
382
+ # ── LM Head & Loss Computation ────────────────────────────────────────
383
+ loss = None
384
+ if labels is not None:
385
+ # Shift for next-token prediction
386
+ shift_h = h[:, :-1, :].contiguous()
387
+ shift_labels = labels[:, 1:].contiguous().reshape(-1)
388
+
389
+ # Chunked LM head projection to prevent allocating massive (B, T, 206464) FP32 tensor in VRAM
390
+ shift_h_flat = shift_h.reshape(-1, shift_h.shape[-1])
391
+ chunk_size = 1024
392
+ n_tokens = shift_h_flat.shape[0]
393
+ total_ce_loss = 0.0
394
+ valid_tokens = 0
395
+
396
+ for i in range(0, n_tokens, chunk_size):
397
+ h_chunk = shift_h_flat[i : i + chunk_size]
398
+ labels_chunk = shift_labels[i : i + chunk_size]
399
+
400
+ # Check for valid tokens
401
+ mask = (labels_chunk != -100)
402
+ n_valid = mask.sum().item()
403
+ if n_valid == 0:
404
+ continue
405
+
406
+ logits_chunk = self.lm_head(h_chunk).float()
407
+ chunk_loss = F.cross_entropy(
408
+ logits_chunk,
409
+ labels_chunk,
410
+ ignore_index=-100,
411
+ reduction="sum",
412
+ )
413
+ total_ce_loss = total_ce_loss + chunk_loss
414
+ valid_tokens += n_valid
415
+
416
+ ce_loss = total_ce_loss / max(1, valid_tokens) if valid_tokens > 0 else (shift_h.sum() * 0.0)
417
+ avg_aux = total_aux_loss / max(1, n_hse_layers)
418
+ loss = ce_loss + avg_aux
419
+ logits = None if self.training else self.lm_head(h)
420
+ else:
421
+ logits = self.lm_head(h)
422
+
423
+ output = {
424
+ "loss": loss,
425
+ "logits": logits,
426
+ "aux_loss": total_aux_loss / max(1, n_hse_layers),
427
+ }
428
+ if output_hidden_states:
429
+ output["hidden_states"] = all_hidden_states
430
+
431
+ return output
432
+
433
+ @torch.no_grad()
434
+ def generate(
435
+ self,
436
+ input_ids: torch.Tensor,
437
+ max_new_tokens: int = 256,
438
+ temperature: float = 1.0,
439
+ top_p: float = 0.9,
440
+ top_k: int = 50,
441
+ repetition_penalty: float = 1.1,
442
+ eos_token_id: int = 2,
443
+ pad_token_id: int = 0,
444
+ ) -> torch.Tensor:
445
+ """
446
+ Autoregressive generation with top-p/top-k sampling.
447
+ Clears KV caches after generation.
448
+ """
449
+ self.eval()
450
+ B, T_prompt = input_ids.shape
451
+ device = input_ids.device
452
+
453
+ generated = input_ids.clone()
454
+
455
+ # Clear KV caches in MHA layers
456
+ for layer in self.layers:
457
+ if isinstance(layer.seq_mixer, GlobalMHALayer):
458
+ layer.seq_mixer.clear_kv_cache()
459
+
460
+ for step in range(max_new_tokens):
461
+ # Only process new tokens after first step
462
+ if step == 0:
463
+ curr_input = generated
464
+ else:
465
+ curr_input = generated[:, -1:] # only last token
466
+
467
+ outputs = self.forward(curr_input, use_cache=True, return_aux_loss=False)
468
+ logits = outputs["logits"][:, -1, :] # (B, vocab_size)
469
+
470
+ # Repetition penalty
471
+ if repetition_penalty != 1.0:
472
+ for b in range(B):
473
+ for tok_id in set(generated[b].tolist()):
474
+ if logits[b, tok_id] < 0:
475
+ logits[b, tok_id] *= repetition_penalty
476
+ else:
477
+ logits[b, tok_id] /= repetition_penalty
478
+
479
+ # Temperature
480
+ if temperature != 1.0:
481
+ logits = logits / temperature
482
+
483
+ # Top-k filtering
484
+ if top_k > 0:
485
+ topk_vals = torch.topk(logits, min(top_k, logits.shape[-1])).values
486
+ logits = logits.masked_fill(logits < topk_vals[:, -1:], float('-inf'))
487
+
488
+ # Top-p (nucleus) sampling
489
+ if top_p < 1.0:
490
+ sorted_logits, sorted_idx = torch.sort(logits, descending=True)
491
+ cumprobs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
492
+ # Remove tokens beyond cumulative probability top_p
493
+ remove_mask = cumprobs - F.softmax(sorted_logits, dim=-1) > top_p
494
+ sorted_logits[remove_mask] = float('-inf')
495
+ logits = torch.scatter(logits, 1, sorted_idx, sorted_logits)
496
+
497
+ # Sample
498
+ probs = F.softmax(logits, dim=-1)
499
+ next_token = torch.multinomial(probs, num_samples=1) # (B, 1)
500
+
501
+ generated = torch.cat([generated, next_token], dim=1)
502
+
503
+ # Stop if all sequences hit EOS
504
+ if (generated == eos_token_id).any(dim=1).all():
505
+ break
506
+
507
+ # Clear caches
508
+ for layer in self.layers:
509
+ if isinstance(layer.seq_mixer, GlobalMHALayer):
510
+ layer.seq_mixer.clear_kv_cache()
511
+
512
+ return generated
513
+
514
+ def count_parameters(self) -> Dict[str, int]:
515
+ """Count parameters by component."""
516
+ counts = {}
517
+ counts["total"] = sum(p.numel() for p in self.parameters())
518
+ counts["embedding"] = self.embed_tokens.weight.numel()
519
+ counts["arg_layers"] = sum(
520
+ p.numel() for i, l in enumerate(self.layers)
521
+ if isinstance(l.seq_mixer, ARGLayer)
522
+ for p in l.seq_mixer.parameters()
523
+ )
524
+ counts["mha_layers"] = sum(
525
+ p.numel() for l in self.layers
526
+ if isinstance(l.seq_mixer, GlobalMHALayer)
527
+ for p in l.seq_mixer.parameters()
528
+ )
529
+ counts["dense_ffn"] = sum(
530
+ p.numel() for l in self.layers
531
+ if isinstance(l.ffn, DenseSwiGLUFFN)
532
+ for p in l.ffn.parameters()
533
+ )
534
+ counts["hse_ffn_total"] = sum(
535
+ p.numel() for l in self.layers
536
+ if isinstance(l.ffn, HSELayer)
537
+ for p in l.ffn.parameters()
538
+ )
539
+ hse_active = 0
540
+ for l in self.layers:
541
+ if isinstance(l.ffn, HSELayer):
542
+ hse_active += l.ffn.top_k * 3 * l.ffn.d_model * l.ffn.d_ff_expert
543
+ counts["hse_ffn_active"] = hse_active
544
+ return counts
545
+
546
+
547
+ def build_forge_500m(config=None) -> ForgeModel:
548
+ """Build FORGE-500M (500M total params) from config (or default config)."""
549
+ from config import ForgeModelConfig
550
+ if config is None:
551
+ config = ForgeModelConfig()
552
+ model = ForgeModel(config)
553
+ return model
554
+
555
+
556
+ def build_forge_3b(config=None) -> ForgeModel:
557
+ """Alias for build_forge_500m (FORGE architecture downgraded to 1B max params)."""
558
+ return build_forge_500m(config)
model/hse_layer.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HSE (Hierarchical Sparse Expert) FFN Layer.
3
+
4
+ Two-tier MoE:
5
+ Tier 1: Select 1 semantic domain from K1=4 domains
6
+ Tier 2: Select top-2 specialists from K2=8 experts within that domain
7
+ Total: 32 experts, 2 active per token
8
+
9
+ Load balanced at both tiers independently for stable training.
10
+ """
11
+
12
+ from __future__ import annotations
13
+ import math
14
+ from typing import Optional, Tuple, Dict
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ import torch.nn.functional as F
19
+
20
+ from .dgn_norm import build_norm
21
+ from .triton_kernels import fused_swiglu
22
+
23
+
24
+ class FeedForwardExpert(nn.Module):
25
+ """Single SwiGLU expert in the HSE layer."""
26
+
27
+ def __init__(self, d_model: int, d_ff: int):
28
+ super().__init__()
29
+ self.gate = nn.Linear(d_model, d_ff, bias=False)
30
+ self.up = nn.Linear(d_model, d_ff, bias=False)
31
+ self.down = nn.Linear(d_ff, d_model, bias=False)
32
+
33
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
34
+ return self.down(fused_swiglu(self.gate(x), self.up(x)))
35
+
36
+
37
+ class DenseSwiGLUFFN(nn.Module):
38
+ """Standard Dense SwiGLU FFN (used in alternating layers)."""
39
+
40
+ def __init__(
41
+ self,
42
+ d_model: int = 1280,
43
+ d_ff: int = 3200,
44
+ norm_type: str = "dgn",
45
+ dgn_n_groups: int = 16,
46
+ norm_eps: float = 1e-6,
47
+ ):
48
+ super().__init__()
49
+ self.norm = build_norm(norm_type, d_model, dgn_n_groups, norm_eps)
50
+ self.gate = nn.Linear(d_model, d_ff, bias=False)
51
+ self.up = nn.Linear(d_model, d_ff, bias=False)
52
+ self.down = nn.Linear(d_ff, d_model, bias=False)
53
+
54
+ nn.init.normal_(self.gate.weight, std=0.02)
55
+ nn.init.normal_(self.up.weight, std=0.02)
56
+ nn.init.normal_(self.down.weight, std=0.02 / math.sqrt(2))
57
+
58
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
59
+ residual = x
60
+ h = self.norm(x)
61
+ return residual + self.down(fused_swiglu(self.gate(h), self.up(h)))
62
+
63
+
64
+ class HSELayer(nn.Module):
65
+ """
66
+ Hierarchical Sparse Expert FFN Layer.
67
+
68
+ Architecture:
69
+ pre-norm β†’ Tier1-router β†’ select domain d*
70
+ β†’ Tier2-router[d*] β†’ select top-2 experts
71
+ β†’ weighted sum of 2 expert outputs
72
+ β†’ residual add
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ d_model: int = 1280,
78
+ n_domains: int = 4,
79
+ n_experts_per_domain: int = 8,
80
+ top_k: int = 2,
81
+ d_ff_expert: int = 288,
82
+ capacity_factor: float = 1.25,
83
+ expert_dropout: float = 0.1,
84
+ aux_loss_alpha: float = 0.01,
85
+ gumbel_tau_init: float = 1.0,
86
+ norm_type: str = "dgn",
87
+ dgn_n_groups: int = 16,
88
+ norm_eps: float = 1e-6,
89
+ layer_idx: int = 0,
90
+ ):
91
+ super().__init__()
92
+ assert top_k <= n_experts_per_domain
93
+
94
+ self.d_model = d_model
95
+ self.n_domains = n_domains
96
+ self.n_experts_per_domain = n_experts_per_domain
97
+ self.n_experts_total = n_domains * n_experts_per_domain
98
+ self.top_k = top_k
99
+ self.d_ff_expert = d_ff_expert
100
+ self.capacity_factor = capacity_factor
101
+ self.expert_dropout = expert_dropout
102
+ self.aux_loss_alpha = aux_loss_alpha
103
+ self.layer_idx = layer_idx
104
+
105
+ # Pre-norm
106
+ self.norm = build_norm(norm_type, d_model, dgn_n_groups, norm_eps)
107
+
108
+ # ── Tier-1 Router ────────────────────────────────────────────────────
109
+ # Selects semantic domain: d* = argmax(W_r1 Β· x / Ο„)
110
+ self.tier1_router = nn.Linear(d_model, n_domains, bias=False)
111
+ self.gumbel_tau = nn.Parameter(
112
+ torch.tensor(gumbel_tau_init), requires_grad=False # annealed externally
113
+ )
114
+
115
+ # ── Tier-2 Routers (one per domain) ──────────────────────────────────
116
+ self.tier2_routers = nn.ModuleList([
117
+ nn.Linear(d_model, n_experts_per_domain, bias=False)
118
+ for _ in range(n_domains)
119
+ ])
120
+
121
+ # ── Experts ───────────────────────────────────────────────────────────
122
+ self.experts = nn.ModuleList([
123
+ FeedForwardExpert(d_model, d_ff_expert)
124
+ for _ in range(self.n_experts_total)
125
+ ])
126
+
127
+ # ── Dropout for expert regularization ────────────────────────────────
128
+ self.expert_dropout_layer = nn.Dropout(expert_dropout)
129
+
130
+ # ── Aux loss accumulator (for logging) ────────────────────────────────
131
+ self._last_aux_loss: Optional[torch.Tensor] = None
132
+
133
+ self._init_weights()
134
+
135
+ def _init_weights(self):
136
+ """Initialize router weights to near-uniform distribution."""
137
+ nn.init.normal_(self.tier1_router.weight, std=0.02)
138
+ for router in self.tier2_routers:
139
+ nn.init.normal_(router.weight, std=0.02)
140
+ for expert in self.experts:
141
+ nn.init.normal_(expert.gate.weight, std=0.02)
142
+ nn.init.normal_(expert.up.weight, std=0.02)
143
+ nn.init.normal_(expert.down.weight, std=0.02 / math.sqrt(2))
144
+
145
+ def update_gumbel_tau(self, progress: float):
146
+ """
147
+ Anneal Gumbel-softmax temperature from init to final.
148
+ progress: float in [0, 1] representing training progress.
149
+ """
150
+ tau_init = 1.0
151
+ tau_final = 0.1
152
+ tau = tau_init * (tau_final / tau_init) ** progress
153
+ self.gumbel_tau.data.fill_(tau)
154
+
155
+ def _compute_aux_loss(
156
+ self,
157
+ tier1_probs: torch.Tensor, # (N, n_domains)
158
+ domain_idx: torch.Tensor, # (N,)
159
+ x_flat: torch.Tensor, # (N, d_model)
160
+ ) -> torch.Tensor:
161
+ """
162
+ Hierarchical load-balance auxiliary loss.
163
+
164
+ Loss = Ξ± * [Tier1_balance + Tier2_balance]
165
+
166
+ Tier1_balance = n_domains * Ξ£_d [f_d * P_d]
167
+ Tier2_balance = Ξ£_d {n_exp * Ξ£_e [f_{d,e} * P_{d,e}]} (over assigned tokens)
168
+ """
169
+ N = x_flat.shape[0]
170
+
171
+ # Tier-1 balance
172
+ # f_d: fraction of tokens routed to domain d
173
+ one_hot_d = F.one_hot(domain_idx, self.n_domains).float() # (N, n_domains)
174
+ f1 = one_hot_d.mean(0) # (n_domains,)
175
+ P1 = tier1_probs.mean(0) # (n_domains,)
176
+ loss_t1 = self.n_domains * (f1 * P1).sum()
177
+
178
+ # Tier-2 balance (over tokens in each domain)
179
+ loss_t2 = torch.zeros(1, device=x_flat.device)
180
+
181
+ for d in range(self.n_domains):
182
+ mask = (domain_idx == d)
183
+ n_d = mask.sum()
184
+ if n_d < 2:
185
+ continue
186
+
187
+ x_d = x_flat[mask] # (n_d, d_model)
188
+ t2_logits = self.tier2_routers[d](x_d) # (n_d, n_exp)
189
+ t2_probs = F.softmax(t2_logits, dim=-1) # (n_d, n_exp)
190
+
191
+ # Top-k indices for f computation
192
+ topk_idx = t2_logits.topk(self.top_k, dim=-1).indices # (n_d, top_k)
193
+ one_hot_e = F.one_hot(topk_idx, self.n_experts_per_domain).float() # (n_d, k, n_exp)
194
+ f2 = one_hot_e.sum(1).mean(0) / self.top_k # (n_exp,)
195
+ P2 = t2_probs.mean(0) # (n_exp,)
196
+
197
+ loss_t2 = loss_t2 + self.n_experts_per_domain * (f2 * P2).sum()
198
+
199
+ return self.aux_loss_alpha * (loss_t1 + loss_t2)
200
+
201
+ def forward(
202
+ self,
203
+ x: torch.Tensor, # (B, T, d_model)
204
+ return_aux_loss: bool = True,
205
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
206
+ """
207
+ Forward pass with hierarchical routing (vectorized for maximum GPU throughput).
208
+ Returns (output, aux_loss) where aux_loss is for load balancing.
209
+ """
210
+ B, T, D = x.shape
211
+ N = B * T
212
+
213
+ residual = x
214
+ x_norm = self.norm(x)
215
+ x_flat = x_norm.reshape(N, D) # (N, D)
216
+
217
+ # ── Tier-1 Routing ────────────────────────────────────────────────────
218
+ tier1_logits = self.tier1_router(x_flat) # (N, n_domains)
219
+ tier1_probs = F.softmax(tier1_logits / float(self.gumbel_tau), dim=-1)
220
+
221
+ if self.training:
222
+ domain_idx = F.gumbel_softmax(
223
+ tier1_logits,
224
+ tau=float(self.gumbel_tau),
225
+ hard=True
226
+ ).argmax(dim=-1) # (N,)
227
+ else:
228
+ domain_idx = tier1_probs.argmax(dim=-1) # (N,)
229
+
230
+ # ── Tier-2 Routing (Zero-sync batched projection + gather) ───────────
231
+ t2_weights = torch.cat([r.weight for r in self.tier2_routers], dim=0) # (32, D)
232
+ t2_all = F.linear(x_flat, t2_weights).view(N, self.n_domains, self.n_experts_per_domain)
233
+
234
+ d_expanded = domain_idx.view(N, 1, 1).expand(-1, 1, self.n_experts_per_domain)
235
+ t2_logits = torch.gather(t2_all, 1, d_expanded).squeeze(1) # (N, 8)
236
+
237
+ t2_probs = F.softmax(t2_logits, dim=-1)
238
+ w, topk_idx = t2_probs.topk(self.top_k, dim=-1)
239
+ w = (w / (w.sum(-1, keepdim=True) + 1e-9)).to(x.dtype)
240
+
241
+ global_exp_idx = topk_idx + (domain_idx.unsqueeze(-1) * self.n_experts_per_domain) # (N, 2)
242
+
243
+ # ── Vectorized Token-Expert Dispatch ──────────────────────────────────
244
+ flat_exp_ids = global_exp_idx.reshape(-1)
245
+ flat_tokens = torch.arange(N, device=x.device).unsqueeze(1).expand(-1, self.top_k).reshape(-1)
246
+ flat_weights = w.reshape(-1, 1)
247
+
248
+ sort_perm = torch.argsort(flat_exp_ids)
249
+ sorted_exp = flat_exp_ids[sort_perm]
250
+ sorted_tokens = flat_tokens[sort_perm]
251
+ sorted_weights = flat_weights[sort_perm]
252
+
253
+ counts = torch.bincount(sorted_exp, minlength=self.n_experts_total).tolist()
254
+ out_flat = torch.zeros(N, D, device=x.device, dtype=x.dtype)
255
+ offset = 0
256
+
257
+ for e in range(self.n_experts_total):
258
+ cnt = counts[e]
259
+ if cnt > 0:
260
+ t_idx = sorted_tokens[offset : offset + cnt]
261
+ w_e = sorted_weights[offset : offset + cnt]
262
+ x_e = x_flat[t_idx]
263
+ if self.training and self.expert_dropout > 0:
264
+ x_e = self.expert_dropout_layer(x_e)
265
+ y_e = self.experts[e](x_e)
266
+ out_flat.index_add_(0, t_idx, w_e * y_e)
267
+ offset += cnt
268
+
269
+ output = out_flat.reshape(B, T, D)
270
+
271
+ # ── Auxiliary loss ────────────────────────────────────────────────────
272
+ aux_loss = None
273
+ if return_aux_loss and self.training:
274
+ aux_loss = self._compute_aux_loss(tier1_probs, domain_idx, x_flat)
275
+ self._last_aux_loss = aux_loss.detach()
276
+
277
+ return residual + output, aux_loss
model/mha_layer.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Full Multi-Head Attention Layer for FORGE's global attention blocks.
3
+ Uses FlashAttention-3 when available, with GQA and RoPE.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ from typing import Optional, Tuple
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ from einops import rearrange
13
+
14
+ from .dgn_norm import build_norm
15
+ from .rotary_embedding import RotaryEmbedding
16
+
17
+ try:
18
+ from flash_attn import flash_attn_func
19
+ FLASH_ATTN = True
20
+ except ImportError:
21
+ FLASH_ATTN = False
22
+
23
+
24
+ class GlobalMHALayer(nn.Module):
25
+ """
26
+ Global Multi-Head Attention with:
27
+ - GQA (Grouped Query Attention) with n_kv_heads < n_heads
28
+ - RoPE positional encoding (long-context base 500k)
29
+ - FlashAttention-3 for maximum GPU utilization
30
+ - KV cache support for efficient inference
31
+ - Pre-norm with DGN
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ d_model: int = 1280,
37
+ n_heads: int = 16,
38
+ n_kv_heads: int = 4,
39
+ head_dim: int = 80,
40
+ rope_base: float = 500_000.0,
41
+ rope_scaling_type: Optional[str] = None,
42
+ rope_scaling_factor: float = 1.0,
43
+ max_seq_len: int = 4096,
44
+ dropout: float = 0.0,
45
+ norm_type: str = "dgn",
46
+ dgn_n_groups: int = 16,
47
+ norm_eps: float = 1e-6,
48
+ use_flash_attention: bool = True,
49
+ layer_idx: int = 0,
50
+ ):
51
+ super().__init__()
52
+ assert d_model == n_heads * head_dim, \
53
+ f"d_model={d_model} must equal n_heads*head_dim={n_heads*head_dim}"
54
+ assert n_heads % n_kv_heads == 0, \
55
+ f"n_heads={n_heads} must be divisible by n_kv_heads={n_kv_heads}"
56
+
57
+ self.d_model = d_model
58
+ self.n_heads = n_heads
59
+ self.n_kv_heads = n_kv_heads
60
+ self.head_dim = head_dim
61
+ self.n_rep = n_heads // n_kv_heads # GQA repetition factor
62
+ self.dropout = dropout
63
+ self.use_flash = use_flash_attention and FLASH_ATTN
64
+ self.layer_idx = layer_idx
65
+ self.scale = head_dim ** -0.5
66
+
67
+ # Pre-norm
68
+ self.norm = build_norm(norm_type, d_model, dgn_n_groups, norm_eps)
69
+
70
+ # Q/K/V projections
71
+ self.q_proj = nn.Linear(d_model, n_heads * head_dim, bias=False)
72
+ self.k_proj = nn.Linear(d_model, n_kv_heads * head_dim, bias=False)
73
+ self.v_proj = nn.Linear(d_model, n_kv_heads * head_dim, bias=False)
74
+ self.o_proj = nn.Linear(d_model, d_model, bias=False)
75
+
76
+ # RoPE
77
+ self.rope = RotaryEmbedding(
78
+ head_dim=head_dim,
79
+ max_seq_len=max_seq_len,
80
+ base=rope_base,
81
+ scaling_type=rope_scaling_type,
82
+ scaling_factor=rope_scaling_factor,
83
+ )
84
+
85
+ # KV cache for inference
86
+ self._kv_cache_k: Optional[torch.Tensor] = None
87
+ self._kv_cache_v: Optional[torch.Tensor] = None
88
+ self._kv_cache_len: int = 0
89
+
90
+ self._init_weights()
91
+
92
+ def _init_weights(self):
93
+ nn.init.normal_(self.q_proj.weight, std=0.02)
94
+ nn.init.normal_(self.k_proj.weight, std=0.02)
95
+ nn.init.normal_(self.v_proj.weight, std=0.02)
96
+ # Output projection: scale by 1/sqrt(2 * n_layers) for residual depth
97
+ nn.init.normal_(self.o_proj.weight, std=0.02)
98
+
99
+ def clear_kv_cache(self):
100
+ self._kv_cache_k = None
101
+ self._kv_cache_v = None
102
+ self._kv_cache_len = 0
103
+
104
+ def forward(
105
+ self,
106
+ x: torch.Tensor, # (B, T, d_model)
107
+ attention_mask: Optional[torch.Tensor] = None, # (B, T) or (B, 1, T, T)
108
+ position_ids: Optional[torch.Tensor] = None,
109
+ use_cache: bool = False,
110
+ ) -> Tuple[torch.Tensor, Optional[Tuple]]:
111
+
112
+ B, T, _ = x.shape
113
+ residual = x
114
+ x_norm = self.norm(x)
115
+
116
+ # Project
117
+ Q = self.q_proj(x_norm) # (B, T, n_heads * head_dim)
118
+ K = self.k_proj(x_norm) # (B, T, n_kv_heads * head_dim)
119
+ V = self.v_proj(x_norm) # (B, T, n_kv_heads * head_dim)
120
+
121
+ # Reshape to (B, H, T, D)
122
+ Q = rearrange(Q, 'b t (h d) -> b h t d', d=self.head_dim)
123
+ K = rearrange(K, 'b t (h d) -> b h t d', d=self.head_dim)
124
+ V = rearrange(V, 'b t (h d) -> b h t d', d=self.head_dim)
125
+
126
+ # RoPE
127
+ Q, K = self.rope.apply_rotary(Q, K, position_ids)
128
+
129
+ # KV cache concat (inference)
130
+ if use_cache:
131
+ if self._kv_cache_k is not None:
132
+ K = torch.cat([self._kv_cache_k, K], dim=2)
133
+ V = torch.cat([self._kv_cache_v, V], dim=2)
134
+ self._kv_cache_k = K
135
+ self._kv_cache_v = V
136
+ self._kv_cache_len = K.shape[2]
137
+
138
+ # GQA: expand K,V from n_kv_heads to n_heads
139
+ if self.n_rep > 1:
140
+ K = K.repeat_interleave(self.n_rep, dim=1)
141
+ V = V.repeat_interleave(self.n_rep, dim=1)
142
+
143
+ # Attention
144
+ if self.use_flash:
145
+ # FlashAttention: expects (B, T, H, D)
146
+ Q_fa = rearrange(Q, 'b h t d -> b t h d').contiguous()
147
+ K_fa = rearrange(K, 'b h t d -> b t h d').contiguous()
148
+ V_fa = rearrange(V, 'b h t d -> b t h d').contiguous()
149
+
150
+ out = flash_attn_func(
151
+ Q_fa, K_fa, V_fa,
152
+ dropout_p=self.dropout if self.training else 0.0,
153
+ causal=True,
154
+ softmax_scale=self.scale,
155
+ ) # (B, T, H, D)
156
+ out = rearrange(out, 'b t h d -> b t (h d)')
157
+ else:
158
+ # Standard PyTorch SDPA (fused, memory efficient)
159
+ out = F.scaled_dot_product_attention(
160
+ Q, K, V,
161
+ attn_mask=attention_mask,
162
+ dropout_p=self.dropout if self.training else 0.0,
163
+ is_causal=(attention_mask is None),
164
+ scale=self.scale,
165
+ ) # (B, H, T, D)
166
+ out = rearrange(out, 'b h t d -> b t (h d)')
167
+
168
+ out = self.o_proj(out) # (B, T, d_model)
169
+
170
+ return residual + out, (K, V) if use_cache else None
model/pscan.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Parallel Scan (Blelloch algorithm) in pure PyTorch for ARG selective SSM recurrence.
3
+
4
+ Eliminates all external dependencies (mamba-ssm, selective_scan_cuda).
5
+ Runs in 2*log2(T) parallel steps (11 steps for T=2048) on any GPU.
6
+ 100% mathematically exact equivalence to sequential SSM recurrence.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import math
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+
15
+ def npo2(length: int) -> int:
16
+ """Returns the next power of 2 above length."""
17
+ return 2 ** math.ceil(math.log2(length))
18
+
19
+
20
+ def pad_npo2(x: torch.Tensor) -> torch.Tensor:
21
+ """Pads input length dimension (dim 1) to the next power of 2."""
22
+ length = x.size(1)
23
+ len_p2 = npo2(length)
24
+ if len_p2 == length:
25
+ return x
26
+ pad_tuple = (0, 0, 0, 0, 0, len_p2 - length)
27
+ return F.pad(x, pad_tuple, "constant", 0)
28
+
29
+
30
+ class PScan(torch.autograd.Function):
31
+ """
32
+ Computes the parallel associative scan:
33
+ H[t] = A[t] * H[t-1] + X[t] with H[-1] = 0
34
+ in O(log2(T)) parallel steps via Blelloch tree reduction.
35
+ """
36
+
37
+ @staticmethod
38
+ def pscan(A: torch.Tensor, X: torch.Tensor) -> None:
39
+ # A: (B, D, L, N)
40
+ # X: (B, D, L, N)
41
+ B, D, L, _ = A.size()
42
+ num_steps = int(math.log2(L))
43
+
44
+ # ── Up sweep ──────────────────────────────────────────────────────────
45
+ Aa = A
46
+ Xa = X
47
+ for _ in range(num_steps - 2):
48
+ T = Xa.size(2)
49
+ Aa = Aa.view(B, D, T // 2, 2, -1)
50
+ Xa = Xa.view(B, D, T // 2, 2, -1)
51
+
52
+ Xa[:, :, :, 1].add_(Aa[:, :, :, 1].mul(Xa[:, :, :, 0]))
53
+ Aa[:, :, :, 1].mul_(Aa[:, :, :, 0])
54
+
55
+ Aa = Aa[:, :, :, 1]
56
+ Xa = Xa[:, :, :, 1]
57
+
58
+ if Xa.size(2) == 4:
59
+ Xa[:, :, 1].add_(Aa[:, :, 1].mul(Xa[:, :, 0]))
60
+ Aa[:, :, 1].mul_(Aa[:, :, 0])
61
+ Xa[:, :, 3].add_(Aa[:, :, 3].mul(Xa[:, :, 2] + Aa[:, :, 2].mul(Xa[:, :, 1])))
62
+ elif Xa.size(2) == 2:
63
+ Xa[:, :, 1].add_(Aa[:, :, 1].mul(Xa[:, :, 0]))
64
+ return
65
+ else:
66
+ return
67
+
68
+ # ── Down sweep ────────────────────────────────────────────────────────
69
+ Aa = A[:, :, 2 ** (num_steps - 2) - 1 : L : 2 ** (num_steps - 2)]
70
+ Xa = X[:, :, 2 ** (num_steps - 2) - 1 : L : 2 ** (num_steps - 2)]
71
+ Xa[:, :, 2].add_(Aa[:, :, 2].mul(Xa[:, :, 1]))
72
+ Aa[:, :, 2].mul_(Aa[:, :, 1])
73
+
74
+ for k in range(num_steps - 3, -1, -1):
75
+ stride = 2 ** k
76
+ Aa = A[:, :, stride - 1 : L : stride]
77
+ Xa = X[:, :, stride - 1 : L : stride]
78
+
79
+ T = Xa.size(2)
80
+ Aa = Aa.view(B, D, T // 2, 2, -1)
81
+ Xa = Xa.view(B, D, T // 2, 2, -1)
82
+
83
+ Xa[:, :, 1:, 0].add_(Aa[:, :, 1:, 0].mul(Xa[:, :, :-1, 1]))
84
+ Aa[:, :, 1:, 0].mul_(Aa[:, :, :-1, 1])
85
+
86
+ @staticmethod
87
+ def pscan_rev(A: torch.Tensor, X: torch.Tensor) -> None:
88
+ # Backward scan in reverse time order
89
+ B, D, L, _ = A.size()
90
+ num_steps = int(math.log2(L))
91
+
92
+ # Up sweep
93
+ Aa = A
94
+ Xa = X
95
+ for _ in range(num_steps - 2):
96
+ T = Xa.size(2)
97
+ Aa = Aa.view(B, D, T // 2, 2, -1)
98
+ Xa = Xa.view(B, D, T // 2, 2, -1)
99
+
100
+ Xa[:, :, :, 0].add_(Aa[:, :, :, 0].mul(Xa[:, :, :, 1]))
101
+ Aa[:, :, :, 0].mul_(Aa[:, :, :, 1])
102
+
103
+ Aa = Aa[:, :, :, 0]
104
+ Xa = Xa[:, :, :, 0]
105
+
106
+ if Xa.size(2) == 4:
107
+ Xa[:, :, 2].add_(Aa[:, :, 2].mul(Xa[:, :, 3]))
108
+ Aa[:, :, 2].mul_(Aa[:, :, 3])
109
+ Xa[:, :, 0].add_(Aa[:, :, 0].mul(Xa[:, :, 1].add(Aa[:, :, 1].mul(Xa[:, :, 2]))))
110
+ elif Xa.size(2) == 2:
111
+ Xa[:, :, 0].add_(Aa[:, :, 0].mul(Xa[:, :, 1]))
112
+ return
113
+ else:
114
+ return
115
+
116
+ # Down sweep
117
+ stride_top = 2 ** (num_steps - 2)
118
+ Aa = A[:, :, 0 : L : stride_top]
119
+ Xa = X[:, :, 0 : L : stride_top]
120
+ Xa[:, :, 1].add_(Aa[:, :, 1].mul(Xa[:, :, 2]))
121
+ Aa[:, :, 1].mul_(Aa[:, :, 2])
122
+
123
+ for k in range(num_steps - 3, -1, -1):
124
+ stride = 2 ** k
125
+ Aa = A[:, :, 0 : L : stride]
126
+ Xa = X[:, :, 0 : L : stride]
127
+
128
+ T = Xa.size(2)
129
+ Aa = Aa.view(B, D, T // 2, 2, -1)
130
+ Xa = Xa.view(B, D, T // 2, 2, -1)
131
+
132
+ Xa[:, :, :-1, 1].add_(Aa[:, :, :-1, 1].mul(Xa[:, :, 1:, 0]))
133
+ Aa[:, :, :-1, 1].mul_(Aa[:, :, 1:, 0])
134
+
135
+ @staticmethod
136
+ def forward(ctx, A_in: torch.Tensor, X_in: torch.Tensor) -> torch.Tensor:
137
+ # A_in: (B, L, D, N)
138
+ # X_in: (B, L, D, N)
139
+ L = X_in.size(1)
140
+
141
+ if L == npo2(L):
142
+ A = A_in.clone()
143
+ X = X_in.clone()
144
+ else:
145
+ A = pad_npo2(A_in).clone()
146
+ X = pad_npo2(X_in).clone()
147
+
148
+ A = A.transpose(2, 1).contiguous() # (B, D, L_p2, N)
149
+ X = X.transpose(2, 1).contiguous() # (B, D, L_p2, N)
150
+
151
+ PScan.pscan(A, X)
152
+
153
+ ctx.save_for_backward(A_in, X)
154
+ return X.transpose(2, 1)[:, :L].contiguous()
155
+
156
+ @staticmethod
157
+ def backward(ctx, grad_output_in: torch.Tensor):
158
+ A_in, X = ctx.saved_tensors
159
+ L = grad_output_in.size(1)
160
+
161
+ if L == npo2(L):
162
+ grad_output = grad_output_in.clone()
163
+ else:
164
+ grad_output = pad_npo2(grad_output_in).clone()
165
+ A_in = pad_npo2(A_in)
166
+
167
+ grad_output = grad_output.transpose(2, 1).contiguous() # (B, D, L_p2, N)
168
+ A_in_t = A_in.transpose(2, 1).contiguous() # (B, D, L_p2, N)
169
+
170
+ # Shift A 1 step to the left for adjoint recurrence
171
+ A_shifted = F.pad(A_in_t[:, :, 1:], (0, 0, 0, 1))
172
+
173
+ PScan.pscan_rev(A_shifted, grad_output)
174
+
175
+ grad_A = torch.zeros_like(X)
176
+ grad_A[:, :, 1:].add_(X[:, :, :-1] * grad_output[:, :, 1:])
177
+
178
+ return grad_A.transpose(2, 1)[:, :L].contiguous(), grad_output.transpose(2, 1)[:, :L].contiguous()
179
+
180
+
181
+ pscan = PScan.apply
model/rotary_embedding.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RoPE (Rotary Positional Embedding) with YaRN support.
3
+ GPU-optimized via cached cos/sin buffers and optional Triton kernel.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import math
8
+ import torch
9
+ import torch.nn as nn
10
+ from typing import Optional, Tuple
11
+
12
+
13
+ class RotaryEmbedding(nn.Module):
14
+ """
15
+ Rotary Positional Embedding (RoPE) with:
16
+ - Long-context theta (500,000 base)
17
+ - YaRN scaling for context extension
18
+ - GPU-resident cos/sin cache (no recomputation per step)
19
+ - BF16-native computation
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ head_dim: int,
25
+ max_seq_len: int = 4096,
26
+ base: float = 500_000.0,
27
+ scaling_type: Optional[str] = None,
28
+ scaling_factor: float = 1.0,
29
+ device: Optional[torch.device] = None,
30
+ ):
31
+ super().__init__()
32
+ self.head_dim = head_dim
33
+ self.max_seq_len = max_seq_len
34
+ self.base = base
35
+ self.scaling_type = scaling_type
36
+ self.scaling_factor = scaling_factor
37
+
38
+ # Compute inverse frequencies
39
+ inv_freq = self._build_inv_freq(head_dim, base, scaling_type, scaling_factor)
40
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
41
+
42
+ # Pre-compute and cache cos/sin for all positions
43
+ self._cos_cache: Optional[torch.Tensor] = None
44
+ self._sin_cache: Optional[torch.Tensor] = None
45
+ self._cache_seq_len: int = 0
46
+
47
+ # Build initial cache
48
+ self._build_cache(max_seq_len, device)
49
+
50
+ def _build_inv_freq(
51
+ self,
52
+ head_dim: int,
53
+ base: float,
54
+ scaling_type: Optional[str],
55
+ scaling_factor: float,
56
+ ) -> torch.Tensor:
57
+ """Compute inverse frequencies with optional YaRN scaling."""
58
+ dim = head_dim
59
+ # Standard RoPE frequencies
60
+ freqs = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
61
+
62
+ if scaling_type == "yarn":
63
+ # YaRN: scale only the low-frequency dimensions
64
+ # High-frequency dims (short-range) are left unchanged
65
+ # Low-frequency dims (long-range) are scaled down by 1/factor
66
+ mscale = 0.1 * math.log(scaling_factor) + 1.0 # YaRN mscale
67
+
68
+ # Threshold: dims where wavelength > original_max_len
69
+ low_freq_factor = 1.0
70
+ high_freq_factor = 4.0
71
+ original_max_len = self.max_seq_len
72
+ new_base = base * (
73
+ (scaling_factor * original_max_len / (2 * math.pi)) -
74
+ (original_max_len / (2 * math.pi))
75
+ ) ** (dim / (dim - 2))
76
+
77
+ # Apply NTK-by-parts
78
+ d = torch.arange(0, dim, 2).float()
79
+ wavelength = 2 * math.pi / freqs
80
+
81
+ # Smooth interpolation between no-scale and full-scale
82
+ scale = torch.where(
83
+ wavelength < original_max_len / high_freq_factor,
84
+ torch.ones_like(freqs),
85
+ torch.where(
86
+ wavelength > original_max_len / low_freq_factor,
87
+ torch.full_like(freqs, 1.0 / scaling_factor),
88
+ (original_max_len / wavelength / low_freq_factor - 1.0) /
89
+ (high_freq_factor / low_freq_factor - 1.0) *
90
+ (1.0 - 1.0 / scaling_factor) + 1.0 / scaling_factor
91
+ )
92
+ )
93
+ freqs = freqs * scale
94
+
95
+ elif scaling_type == "linear":
96
+ freqs = freqs / scaling_factor
97
+
98
+ return freqs
99
+
100
+ @torch.no_grad()
101
+ def _build_cache(self, seq_len: int, device=None):
102
+ """Build/extend the cos/sin cache up to seq_len."""
103
+ if seq_len <= self._cache_seq_len:
104
+ return
105
+
106
+ device = device or (self.inv_freq.device if hasattr(self, 'inv_freq')
107
+ else torch.device('cpu'))
108
+
109
+ t = torch.arange(seq_len, device=device, dtype=torch.float32)
110
+ freqs = torch.outer(t, self.inv_freq.to(device))
111
+ emb = torch.cat([freqs, freqs], dim=-1) # (seq_len, head_dim)
112
+
113
+ # Store in BF16 for fast loading
114
+ self._cos_cache = emb.cos().to(torch.bfloat16)
115
+ self._sin_cache = emb.sin().to(torch.bfloat16)
116
+ self._cache_seq_len = seq_len
117
+
118
+ def get_cos_sin(self, seq_len: int, device: torch.device) -> Tuple[torch.Tensor, torch.Tensor]:
119
+ """Return cached cos/sin for positions 0..seq_len-1."""
120
+ if seq_len > self._cache_seq_len:
121
+ self._build_cache(seq_len * 2, device) # extend with buffer
122
+
123
+ cos = self._cos_cache[:seq_len].to(device)
124
+ sin = self._sin_cache[:seq_len].to(device)
125
+ return cos, sin # each (seq_len, head_dim)
126
+
127
+ def apply_rotary(
128
+ self,
129
+ q: torch.Tensor, # (B, n_heads, T, head_dim)
130
+ k: torch.Tensor, # (B, n_kv_heads, T, head_dim)
131
+ position_ids: Optional[torch.Tensor] = None,
132
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
133
+ """Apply RoPE to Q and K."""
134
+ T = q.shape[2]
135
+ device = q.device
136
+
137
+ cos, sin = self.get_cos_sin(T, device) # (T, head_dim)
138
+
139
+ if position_ids is not None:
140
+ # Custom positions (for packed sequences or position bias)
141
+ cos = cos[position_ids] # (B, T, head_dim) or (T, head_dim)
142
+ sin = sin[position_ids]
143
+ if position_ids.dim() == 2:
144
+ cos = cos.unsqueeze(1) # (B, 1, T, head_dim)
145
+ sin = sin.unsqueeze(1)
146
+ else:
147
+ cos = cos.unsqueeze(0).unsqueeze(0) # (1, 1, T, head_dim)
148
+ sin = sin.unsqueeze(0).unsqueeze(0)
149
+ else:
150
+ # Reshape for broadcasting: (1, 1, T, head_dim)
151
+ cos = cos.unsqueeze(0).unsqueeze(0)
152
+ sin = sin.unsqueeze(0).unsqueeze(0)
153
+
154
+ q_out = _rotate_half_apply(q.float(), cos, sin).to(q.dtype)
155
+ k_out = _rotate_half_apply(k.float(), cos, sin).to(k.dtype)
156
+
157
+ return q_out, k_out
158
+
159
+ def forward(self, q, k, position_ids=None):
160
+ return self.apply_rotary(q, k, position_ids)
161
+
162
+
163
+ def _rotate_half_apply(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
164
+ """Apply rotary: x * cos + rotate_half(x) * sin"""
165
+ d = x.shape[-1] // 2
166
+ x1, x2 = x[..., :d], x[..., d:]
167
+ rotated = torch.cat([-x2, x1], dim=-1)
168
+ return x * cos + rotated * sin
model/triton_kernels.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORGE-3B Triton GPU Kernels
3
+ Hand-written Triton kernels for maximum GPU throughput:
4
+ - Fused RMSNorm / DGN (Differential Group Normalization)
5
+ - Fused SwiGLU activation
6
+ - Complex SSM scan kernel
7
+ - Fused MoE dispatch + expert compute + gather
8
+ - Fused RoPE injection
9
+
10
+ All kernels target H100/A100 with BF16 inputs.
11
+ """
12
+
13
+ from __future__ import annotations
14
+ import math
15
+ import torch
16
+ import torch.nn.functional as F
17
+
18
+ import os
19
+ try:
20
+ import triton
21
+ import triton.language as tl
22
+ TRITON_AVAILABLE = os.environ.get("FORGE_NO_TRITON") != "1"
23
+ except ImportError:
24
+ TRITON_AVAILABLE = False
25
+ import warnings
26
+ warnings.warn("Triton not available β€” falling back to PyTorch ops. "
27
+ "Install triton for 2-4Γ— kernel speedup.")
28
+
29
+
30
+ # ─────────────────────────────────────────────────────────────────────────────
31
+ # FUSED RMSNorm KERNEL
32
+ # ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ if TRITON_AVAILABLE:
35
+ @triton.jit
36
+ def _rms_norm_fwd_kernel(
37
+ X, # input ptr (N, D)
38
+ W, # weight ptr (D,)
39
+ B, # bias ptr (D,) or None
40
+ Y, # output ptr (N, D)
41
+ Rstd, # 1/rms ptr (N,) β€” saved for backward
42
+ stride_x, # stride along N dim
43
+ N, # number of rows
44
+ D, # row dimension
45
+ eps,
46
+ HAS_BIAS: tl.constexpr,
47
+ BLOCK_D: tl.constexpr,
48
+ ):
49
+ row = tl.program_id(0)
50
+ X_ptr = X + row * stride_x
51
+ Y_ptr = Y + row * stride_x
52
+
53
+ # Compute mean square
54
+ ms = tl.zeros([BLOCK_D], dtype=tl.float32)
55
+ for off in range(0, D, BLOCK_D):
56
+ cols = off + tl.arange(0, BLOCK_D)
57
+ mask = cols < D
58
+ x = tl.load(X_ptr + cols, mask=mask, other=0.0).to(tl.float32)
59
+ ms += x * x
60
+ ms = tl.sum(ms, axis=0) / D
61
+ rstd = 1.0 / tl.sqrt(ms + eps)
62
+ tl.store(Rstd + row, rstd)
63
+
64
+ # Normalize and scale
65
+ for off in range(0, D, BLOCK_D):
66
+ cols = off + tl.arange(0, BLOCK_D)
67
+ mask = cols < D
68
+ x = tl.load(X_ptr + cols, mask=mask, other=0.0).to(tl.float32)
69
+ w = tl.load(W + cols, mask=mask, other=1.0).to(tl.float32)
70
+ y = x * rstd * w
71
+ if HAS_BIAS:
72
+ b = tl.load(B + cols, mask=mask, other=0.0).to(tl.float32)
73
+ y = y + b
74
+ tl.store(Y_ptr + cols, y.to(tl.bfloat16), mask=mask)
75
+
76
+
77
+ @triton.jit
78
+ def _dgn_fwd_kernel(
79
+ X, # (N, D) input
80
+ W, # (G, Dg) per-group weight
81
+ B, # (G, Dg) per-group bias
82
+ Y, # (N, D) output
83
+ Rstd, # (N, G) per-group rstd
84
+ stride_x,
85
+ N, D, G, # n_rows, d_model, n_groups
86
+ Dg, # group size = D / G
87
+ eps,
88
+ BLOCK_Dg: tl.constexpr,
89
+ ):
90
+ """Differential Group Normalization: separate RMS per feature group."""
91
+ row = tl.program_id(0)
92
+ grp = tl.program_id(1)
93
+
94
+ base_x = row * stride_x + grp * Dg
95
+
96
+ # Compute group RMS
97
+ ms = tl.zeros([BLOCK_Dg], dtype=tl.float32)
98
+ for off in range(0, Dg, BLOCK_Dg):
99
+ cols = off + tl.arange(0, BLOCK_Dg)
100
+ mask = cols < Dg
101
+ x = tl.load(X + base_x + cols, mask=mask, other=0.0).to(tl.float32)
102
+ ms += x * x
103
+ ms = tl.sum(ms, axis=0) / Dg
104
+ rstd = 1.0 / tl.sqrt(ms + eps)
105
+ tl.store(Rstd + row * G + grp, rstd)
106
+
107
+ # Normalize, scale, bias
108
+ for off in range(0, Dg, BLOCK_Dg):
109
+ cols = off + tl.arange(0, BLOCK_Dg)
110
+ mask = cols < Dg
111
+ x = tl.load(X + base_x + cols, mask=mask, other=0.0).to(tl.float32)
112
+ w = tl.load(W + grp * Dg + cols, mask=mask, other=1.0).to(tl.float32)
113
+ b = tl.load(B + grp * Dg + cols, mask=mask, other=0.0).to(tl.float32)
114
+ y = x * rstd * w + b
115
+ tl.store(Y + base_x + cols, y.to(tl.bfloat16), mask=mask)
116
+
117
+
118
+ @triton.jit
119
+ def _swiglu_fwd_kernel(
120
+ Gate, # (N, D) gate branch
121
+ Up, # (N, D) up branch
122
+ Out, # (N, D) output = silu(Gate) * Up
123
+ N, D,
124
+ BLOCK_D: tl.constexpr,
125
+ ):
126
+ """Fused SwiGLU: Out = SiLU(Gate) * Up"""
127
+ row = tl.program_id(0)
128
+ for off in range(0, D, BLOCK_D):
129
+ cols = off + tl.arange(0, BLOCK_D)
130
+ mask = cols < D
131
+ g = tl.load(Gate + row * D + cols, mask=mask).to(tl.float32)
132
+ u = tl.load(Up + row * D + cols, mask=mask).to(tl.float32)
133
+ # SiLU(x) = x * sigmoid(x)
134
+ silu_g = g * tl.sigmoid(g)
135
+ out = silu_g * u
136
+ tl.store(Out + row * D + cols, out.to(tl.bfloat16), mask=mask)
137
+
138
+
139
+ @triton.jit
140
+ def _rope_fwd_kernel(
141
+ Q, # (B, H, T, D) queries
142
+ K, # (B, H_kv, T, D) keys
143
+ Cos, # (T, D/2) cosine
144
+ Sin, # (T, D/2) sine
145
+ Q_out,
146
+ K_out,
147
+ B, H_q, H_kv, T, D,
148
+ BLOCK_T: tl.constexpr,
149
+ BLOCK_D: tl.constexpr,
150
+ ):
151
+ """Fused RoPE for Q and K."""
152
+ b_idx = tl.program_id(0)
153
+ h_idx = tl.program_id(1)
154
+ is_kv_head = h_idx >= H_q
155
+
156
+ for t in range(0, T, BLOCK_T):
157
+ t_off = t + tl.arange(0, BLOCK_T)
158
+ t_mask = t_off < T
159
+
160
+ for d in range(0, D // 2, BLOCK_D):
161
+ d_off = d + tl.arange(0, BLOCK_D)
162
+ d_mask = d_off < (D // 2)
163
+
164
+ # Load cos/sin
165
+ cos = tl.load(Cos + t_off[:, None] * (D // 2) + d_off[None, :],
166
+ mask=t_mask[:, None] & d_mask[None, :], other=1.0).to(tl.float32)
167
+ sin = tl.load(Sin + t_off[:, None] * (D // 2) + d_off[None, :],
168
+ mask=t_mask[:, None] & d_mask[None, :], other=0.0).to(tl.float32)
169
+
170
+ if not is_kv_head:
171
+ ptr_base = Q + b_idx * H_q * T * D + h_idx * T * D
172
+ out_base = Q_out + b_idx * H_q * T * D + h_idx * T * D
173
+ # x1 = x[..., :D/2], x2 = x[..., D/2:]
174
+ x1 = tl.load(ptr_base + t_off[:, None] * D + d_off[None, :],
175
+ mask=t_mask[:, None] & d_mask[None, :]).to(tl.float32)
176
+ x2 = tl.load(ptr_base + t_off[:, None] * D + (D // 2) + d_off[None, :],
177
+ mask=t_mask[:, None] & d_mask[None, :]).to(tl.float32)
178
+ y1 = x1 * cos - x2 * sin
179
+ y2 = x1 * sin + x2 * cos
180
+ tl.store(out_base + t_off[:, None] * D + d_off[None, :],
181
+ y1.to(tl.bfloat16), mask=t_mask[:, None] & d_mask[None, :])
182
+ tl.store(out_base + t_off[:, None] * D + (D // 2) + d_off[None, :],
183
+ y2.to(tl.bfloat16), mask=t_mask[:, None] & d_mask[None, :])
184
+
185
+
186
+ # ─────────────────────────────────────────────────────────────────────────────
187
+ # PYTHON WRAPPERS AROUND TRITON KERNELS
188
+ # ─────────────────────────────────────────────────────────────────────────────
189
+
190
+ class FusedDGN(torch.autograd.Function):
191
+ """
192
+ Fused Differential Group Normalization using Triton kernel.
193
+ Fallback to PyTorch if Triton unavailable.
194
+ """
195
+
196
+ @staticmethod
197
+ def forward(ctx, x: torch.Tensor, weight: torch.Tensor,
198
+ bias: torch.Tensor, n_groups: int, eps: float):
199
+ """
200
+ x: (batch, seq, d_model) or (N, d_model)
201
+ weight, bias: (d_model,)
202
+ """
203
+ orig_shape = x.shape
204
+ ctx.orig_shape = orig_shape
205
+ x_2d = x.reshape(-1, x.shape[-1]).contiguous()
206
+ N, D = x_2d.shape
207
+ G = n_groups
208
+ Dg = D // G
209
+
210
+ if not TRITON_AVAILABLE or not x.is_cuda:
211
+ # Pure PyTorch fallback
212
+ out = _dgn_pytorch(x_2d, weight, bias, G, Dg, eps)
213
+ ctx.save_for_backward(x_2d, weight, bias,
214
+ _compute_rstd_pytorch(x_2d, G, Dg, eps))
215
+ ctx.n_groups = G
216
+ ctx.Dg = Dg
217
+ ctx.eps = eps
218
+ return out.reshape(orig_shape)
219
+
220
+ y = torch.empty_like(x_2d)
221
+ rstd = torch.empty(N, G, device=x.device, dtype=torch.float32)
222
+
223
+ BLOCK_Dg = min(triton.next_power_of_2(Dg), 128)
224
+ grid = (N, G)
225
+
226
+ _dgn_fwd_kernel[grid](
227
+ x_2d, weight.reshape(G, Dg), bias.reshape(G, Dg),
228
+ y, rstd,
229
+ x_2d.stride(0), N, D, G, Dg, eps,
230
+ BLOCK_Dg=BLOCK_Dg,
231
+ )
232
+
233
+ ctx.save_for_backward(x_2d, weight, bias, rstd)
234
+ ctx.n_groups = G
235
+ ctx.Dg = Dg
236
+ ctx.eps = eps
237
+ return y.reshape(orig_shape)
238
+
239
+ @staticmethod
240
+ def backward(ctx, grad_output):
241
+ x, weight, bias, rstd = ctx.saved_tensors
242
+ G, Dg, eps = ctx.n_groups, ctx.Dg, ctx.eps
243
+
244
+ # Backward via PyTorch autograd (exact, Triton bwd TBD)
245
+ with torch.enable_grad():
246
+ x_r = x.detach().requires_grad_(True)
247
+ w_r = weight.detach().requires_grad_(True)
248
+ b_r = bias.detach().requires_grad_(True)
249
+ y = _dgn_pytorch(x_r, w_r, b_r, G, Dg, eps)
250
+ y.backward(grad_output.reshape_as(y))
251
+
252
+ return x_r.grad.reshape(ctx.orig_shape), w_r.grad, b_r.grad, None, None
253
+
254
+
255
+ def _dgn_pytorch(x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor,
256
+ G: int, Dg: int, eps: float) -> torch.Tensor:
257
+ """PyTorch reference for DGN β€” used in backward and as fallback."""
258
+ N, D = x.shape
259
+ x_g = x.reshape(N, G, Dg)
260
+ rms = x_g.float().pow(2).mean(dim=-1, keepdim=True).add(eps).sqrt()
261
+ x_norm = (x_g / rms).reshape(N, D)
262
+ return x_norm.to(x.dtype) * weight + bias
263
+
264
+
265
+ def _compute_rstd_pytorch(x: torch.Tensor, G: int, Dg: int, eps: float) -> torch.Tensor:
266
+ N, D = x.shape
267
+ x_g = x.reshape(N, G, Dg).float()
268
+ rms = x_g.pow(2).mean(dim=-1).add(eps).sqrt()
269
+ return 1.0 / rms
270
+
271
+
272
+ def fused_dgn(x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor,
273
+ n_groups: int = 16, eps: float = 1e-6) -> torch.Tensor:
274
+ """Entry point for fused DGN β€” uses Triton if available, else PyTorch."""
275
+ return FusedDGN.apply(x, weight, bias, n_groups, eps)
276
+
277
+
278
+ def fused_swiglu(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor:
279
+ """Fused SwiGLU: output = SiLU(gate) * up"""
280
+ if TRITON_AVAILABLE and gate.is_cuda and gate.dtype == torch.bfloat16:
281
+ orig_shape = gate.shape
282
+ gate_2d = gate.reshape(-1, gate.shape[-1]).contiguous()
283
+ up_2d = up.reshape(-1, up.shape[-1]).contiguous()
284
+ N, D = gate_2d.shape
285
+ out = torch.empty_like(gate_2d)
286
+
287
+ BLOCK_D = min(triton.next_power_of_2(D), 512)
288
+ grid = (N,)
289
+ _swiglu_fwd_kernel[grid](gate_2d, up_2d, out, N, D, BLOCK_D=BLOCK_D)
290
+ return out.reshape(orig_shape)
291
+ else:
292
+ return F.silu(gate) * up
293
+
294
+
295
+ # ─────────────────────────────────────────────────────────────────────────────
296
+ # COMPLEX SSM PARALLEL SCAN (Triton)
297
+ # Implements O(T) parallel prefix scan for the ARG recurrent branch.
298
+ # For complex-valued states stored as (real, imag) interleaved.
299
+ # ─────────────────────────────────────────────────────────────────────────────
300
+
301
+
302
+ @torch.jit.script
303
+ def _ssm_forward_scan(
304
+ ar: torch.Tensor, # (B, T, d_state) float32
305
+ ai: torch.Tensor, # (B, T, d_state)
306
+ bx: torch.Tensor, # (B, T, d_state) = B_bar * x_inner[:,:,:d_state]
307
+ h0_real: torch.Tensor, # (B, d_state)
308
+ h0_imag: torch.Tensor, # (B, d_state)
309
+ ) -> tuple[torch.Tensor, torch.Tensor]:
310
+ """TorchScript-compiled sequential SSM scan β€” zero Python overhead per step."""
311
+ B = ar.size(0)
312
+ T = ar.size(1)
313
+ d_state = ar.size(2)
314
+ h_real = torch.empty(B, T + 1, d_state, dtype=ar.dtype, device=ar.device)
315
+ h_imag = torch.empty(B, T + 1, d_state, dtype=ar.dtype, device=ar.device)
316
+ h_real[:, 0] = h0_real
317
+ h_imag[:, 0] = h0_imag
318
+ for t in range(T):
319
+ hr = h_real[:, t]
320
+ hi = h_imag[:, t]
321
+ art = ar[:, t]
322
+ ait = ai[:, t]
323
+ h_real[:, t + 1] = art * hr - ait * hi + bx[:, t]
324
+ h_imag[:, t + 1] = art * hi + ait * hr
325
+ return h_real, h_imag
326
+
327
+
328
+ @torch.jit.script
329
+ def _ssm_backward_scan(
330
+ ar: torch.Tensor, # (B, T, d_state)
331
+ ai: torch.Tensor, # (B, T, d_state)
332
+ bb: torch.Tensor, # (B, T, d_state)
333
+ xi_slice: torch.Tensor, # (B, T, d_state) β€” x_inner[:,:,:d_state]
334
+ cc: torch.Tensor, # (B, T, d_state)
335
+ h_real: torch.Tensor, # (B, T+1, d_state)
336
+ h_imag: torch.Tensor, # (B, T+1, d_state)
337
+ go_scalar: torch.Tensor, # (B, T)
338
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
339
+ """TorchScript-compiled reverse accumulation backward β€” zero Python overhead per step."""
340
+ B = ar.size(0)
341
+ T = ar.size(1)
342
+ d_state = ar.size(2)
343
+
344
+ grad_hreal_from_y = go_scalar.unsqueeze(-1) * cc # (B, T, d_state)
345
+
346
+ grad_ar = torch.zeros_like(ar)
347
+ grad_ai = torch.zeros_like(ai)
348
+ grad_bb = torch.zeros_like(bb)
349
+ grad_xi_ssm = torch.zeros_like(xi_slice)
350
+ delta_hr = torch.zeros(B, d_state, dtype=ar.dtype, device=ar.device)
351
+ delta_hi = torch.zeros(B, d_state, dtype=ar.dtype, device=ar.device)
352
+
353
+ for t in range(T - 1, -1, -1):
354
+ hr_t = h_real[:, t]
355
+ hi_t = h_imag[:, t]
356
+ art = ar[:, t]
357
+ ait = ai[:, t]
358
+
359
+ total_hr = grad_hreal_from_y[:, t] + delta_hr
360
+ total_hi = delta_hi
361
+
362
+ grad_ar[:, t] = total_hr * hr_t + total_hi * hi_t
363
+ grad_ai[:, t] = -total_hr * hi_t + total_hi * hr_t
364
+
365
+ grad_bx_t = total_hr
366
+ grad_bb[:, t] = grad_bx_t * xi_slice[:, t]
367
+ grad_xi_ssm[:, t] = grad_bx_t * bb[:, t]
368
+
369
+ delta_hr = art * total_hr + ait * total_hi
370
+ delta_hi = -ait * total_hr + art * total_hi
371
+
372
+ grad_h0_real = delta_hr
373
+ grad_h0_imag = delta_hi
374
+ return grad_ar, grad_ai, grad_bb, grad_xi_ssm, grad_h0_real, grad_h0_imag
375
+
376
+
377
+ class ComplexSSMScan(torch.autograd.Function):
378
+ """
379
+ Fused complex SSM scan with TorchScript forward + analytic backward.
380
+
381
+ Both the forward scan and backward accumulation loops run inside
382
+ @torch.jit.script functions β€” no Python interpreter overhead per timestep.
383
+ """
384
+
385
+ @staticmethod
386
+ def forward(ctx,
387
+ x_inner: torch.Tensor, # (B, T, d_inner)
388
+ A_bar_real: torch.Tensor, # (B, T, d_state)
389
+ A_bar_imag: torch.Tensor, # (B, T, d_state)
390
+ B_bar: torch.Tensor, # (B, T, d_state)
391
+ C: torch.Tensor, # (B, T, d_state)
392
+ D: torch.Tensor, # (d_inner,)
393
+ h0_real: torch.Tensor, # (B, d_state)
394
+ h0_imag: torch.Tensor, # (B, d_state)
395
+ ):
396
+ B, T, d_inner = x_inner.shape
397
+ d_state = A_bar_real.shape[2]
398
+ dtype = x_inner.dtype
399
+
400
+ ar = A_bar_real.float()
401
+ ai = A_bar_imag.float()
402
+ bb = B_bar.float()
403
+ cc = C.float()
404
+ xi = x_inner.float()
405
+ D_f = D.float()
406
+
407
+ bx = bb * xi[:, :, :d_state] # (B, T, d_state)
408
+
409
+ # TorchScript-compiled forward scan β€” no Python loop overhead
410
+ h_real, h_imag = _ssm_forward_scan(ar, ai, bx, h0_real.float(), h0_imag.float())
411
+
412
+ y_ssm = (cc * h_real[:, 1:]).sum(-1, keepdim=True) # (B, T, 1)
413
+ y = y_ssm.expand(B, T, d_inner) + D_f.unsqueeze(0).unsqueeze(0) * xi
414
+
415
+ ctx.save_for_backward(ar, ai, bb, cc, xi, D_f, h_real, h_imag)
416
+ ctx.input_dtype = dtype
417
+ ctx.d_state = d_state
418
+ return y.to(dtype)
419
+
420
+ @staticmethod
421
+ def backward(ctx, grad_output):
422
+ ar, ai, bb, cc, xi, D_f, h_real, h_imag = ctx.saved_tensors
423
+ B, T, d_inner = grad_output.shape
424
+ d_state = ctx.d_state
425
+ go = grad_output.float()
426
+
427
+ grad_D = (go * xi).sum([0, 1])
428
+ grad_xi = go * D_f.unsqueeze(0).unsqueeze(0)
429
+
430
+ go_scalar = go.sum(-1) # (B, T)
431
+ grad_C = go_scalar.unsqueeze(-1) * h_real[:, 1:] # (B, T, d_state)
432
+
433
+ xi_slice = xi[:, :, :d_state]
434
+
435
+ # TorchScript-compiled backward scan β€” no Python loop overhead
436
+ grad_ar, grad_ai, grad_bb, grad_xi_ssm, grad_h0_real, grad_h0_imag = \
437
+ _ssm_backward_scan(ar, ai, bb, xi_slice, cc, h_real, h_imag, go_scalar)
438
+
439
+ grad_xi[:, :, :d_state] = grad_xi[:, :, :d_state] + grad_xi_ssm
440
+
441
+ dtype = ctx.input_dtype
442
+ return (
443
+ grad_xi.to(dtype),
444
+ grad_ar.to(dtype),
445
+ grad_ai.to(dtype),
446
+ grad_bb.to(dtype),
447
+ grad_C.to(dtype),
448
+ grad_D.to(dtype),
449
+ grad_h0_real.to(dtype),
450
+ grad_h0_imag.to(dtype),
451
+ )
452
+
453
+
454
+ # Export
455
+ def complex_ssm_scan(x_inner, A_bar_real, A_bar_imag, B_bar, C, D, h0_real, h0_imag):
456
+ """Main entry point for complex SSM scan."""
457
+ return ComplexSSMScan.apply(x_inner, A_bar_real, A_bar_imag, B_bar, C, D, h0_real, h0_imag)
model/triton_ssm.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pure Triton Fused Selective State Space Model (SSM) kernel.
3
+ Eliminates mamba_ssm / selective_scan_cuda dependencies entirely.
4
+ Compiles natively on NVIDIA B300 (sm_100), H100 (sm_90), A100 (sm_80).
5
+
6
+ Fuses:
7
+ 1. Discretization: dA = exp(dt * A)
8
+ 2. State input update: u = dt * x * B
9
+ 3. Recurrent state accumulation: h_t = dA * h_{t-1} + u_t
10
+ 4. State output contraction: y_t = sum(h_t * C_t)
11
+ 5. Skip connection: y_t + x_t * D
12
+ 6. Output gating: out_t = (y_t + x_t * D) * silu(z_t)
13
+ """
14
+
15
+ from __future__ import annotations
16
+ import torch
17
+ import triton
18
+ import triton.language as tl
19
+
20
+
21
+ @triton.jit
22
+ def ssm_fwd_kernel(
23
+ # Pointers
24
+ x_ptr, dt_ptr, A_ptr, B_ptr, C_ptr, D_ptr, z_ptr, out_ptr, h_all_ptr,
25
+ # Strides
26
+ stride_xb, stride_xd, stride_xt,
27
+ stride_dtb, stride_dtd, stride_dtt,
28
+ stride_Ad, stride_An,
29
+ stride_Bb, stride_Bn, stride_Bt,
30
+ stride_Cb, stride_Cn, stride_Ct,
31
+ stride_outb, stride_outd, stride_outt,
32
+ stride_hb, stride_hd, stride_ht, stride_hn,
33
+ # Dimensions
34
+ T: tl.constexpr,
35
+ N: tl.constexpr,
36
+ N_BLOCK: tl.constexpr,
37
+ ):
38
+ pid_b = tl.program_id(0)
39
+ pid_d = tl.program_id(1)
40
+
41
+ offs_n = tl.arange(0, N_BLOCK)
42
+ mask_n = offs_n < N
43
+
44
+ # Load continuous-time decay A and skip coefficient D
45
+ A = tl.load(A_ptr + pid_d * stride_Ad + offs_n * stride_An, mask=mask_n, other=0.0)
46
+ D = tl.load(D_ptr + pid_d)
47
+
48
+ # State h accumulator in registers (FP32 precision)
49
+ h = tl.zeros([N_BLOCK], dtype=tl.float32)
50
+
51
+ for t in range(0, T):
52
+ dt_val = tl.load(dt_ptr + pid_b * stride_dtb + pid_d * stride_dtd + t * stride_dtt).to(tl.float32)
53
+ x_val = tl.load(x_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt).to(tl.float32)
54
+ z_val = tl.load(z_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt).to(tl.float32)
55
+
56
+ B_vec = tl.load(B_ptr + pid_b * stride_Bb + offs_n * stride_Bn + t * stride_Bt, mask=mask_n, other=0.0).to(tl.float32)
57
+ C_vec = tl.load(C_ptr + pid_b * stride_Cb + offs_n * stride_Cn + t * stride_Ct, mask=mask_n, other=0.0).to(tl.float32)
58
+
59
+ # Discretize: dA = exp(dt * A)
60
+ dA = tl.exp(dt_val * A)
61
+
62
+ # u = dt * x * B
63
+ u = (dt_val * x_val) * B_vec
64
+
65
+ # State transition: h = dA * h + u
66
+ h = dA * h + u
67
+
68
+ # Save state snapshot for exact backward gradient flow
69
+ tl.store(h_all_ptr + pid_b * stride_hb + pid_d * stride_hd + t * stride_ht + offs_n * stride_hn, h, mask=mask_n)
70
+
71
+ # Output contraction: y = sum(h * C)
72
+ y = tl.sum(tl.where(mask_n, h * C_vec, 0.0), axis=0)
73
+
74
+ # Skip connection + SiLU gate: out = (y + x * D) * silu(z)
75
+ sig_z = 1.0 / (1.0 + tl.exp(-z_val))
76
+ silu_z = z_val * sig_z
77
+ out_val = (y + x_val * D) * silu_z
78
+
79
+ tl.store(out_ptr + pid_b * stride_outb + pid_d * stride_outd + t * stride_outt, out_val)
80
+
81
+
82
+ @triton.jit
83
+ def ssm_bwd_fast_kernel(
84
+ dout_ptr, x_ptr, dt_ptr, A_ptr, B_ptr, C_ptr, D_ptr, z_ptr, h_all_ptr,
85
+ dx_ptr, ddt_ptr, dA_ptr, dD_ptr, dz_ptr, dy_ptr, dh_lambda_ptr,
86
+ stride_xb, stride_xd, stride_xt,
87
+ stride_dtb, stride_dtd, stride_dtt,
88
+ stride_Ad, stride_An,
89
+ stride_Bb, stride_Bn, stride_Bt,
90
+ stride_Cb, stride_Cn, stride_Ct,
91
+ stride_doutb, stride_doutd, stride_doutt,
92
+ stride_hb, stride_hd, stride_ht, stride_hn,
93
+ T: tl.constexpr, N: tl.constexpr, N_BLOCK: tl.constexpr,
94
+ ):
95
+ pid_b = tl.program_id(0)
96
+ pid_d = tl.program_id(1)
97
+
98
+ offs_n = tl.arange(0, N_BLOCK)
99
+ mask_n = offs_n < N
100
+
101
+ A = tl.load(A_ptr + pid_d * stride_Ad + offs_n * stride_An, mask=mask_n, other=0.0)
102
+ D = tl.load(D_ptr + pid_d)
103
+
104
+ lambda_h = tl.zeros([N_BLOCK], dtype=tl.float32)
105
+ accum_dD = 0.0
106
+ accum_dA = tl.zeros([N_BLOCK], dtype=tl.float32)
107
+
108
+ for t in range(T - 1, -1, -1):
109
+ dout_val = tl.load(dout_ptr + pid_b * stride_doutb + pid_d * stride_doutd + t * stride_doutt).to(tl.float32)
110
+ x_val = tl.load(x_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt).to(tl.float32)
111
+ z_val = tl.load(z_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt).to(tl.float32)
112
+ dt_val = tl.load(dt_ptr + pid_b * stride_dtb + pid_d * stride_dtd + t * stride_dtt).to(tl.float32)
113
+
114
+ B_vec = tl.load(B_ptr + pid_b * stride_Bb + offs_n * stride_Bn + t * stride_Bt, mask=mask_n, other=0.0).to(tl.float32)
115
+ C_vec = tl.load(C_ptr + pid_b * stride_Cb + offs_n * stride_Cn + t * stride_Ct, mask=mask_n, other=0.0).to(tl.float32)
116
+ h_t = tl.load(h_all_ptr + pid_b * stride_hb + pid_d * stride_hd + t * stride_ht + offs_n * stride_hn, mask=mask_n, other=0.0).to(tl.float32)
117
+
118
+ if t > 0:
119
+ h_prev = tl.load(h_all_ptr + pid_b * stride_hb + pid_d * stride_hd + (t - 1) * stride_ht + offs_n * stride_hn, mask=mask_n, other=0.0).to(tl.float32)
120
+ else:
121
+ h_prev = tl.zeros([N_BLOCK], dtype=tl.float32)
122
+
123
+ sig_z = 1.0 / (1.0 + tl.exp(-z_val))
124
+ silu_z = z_val * sig_z
125
+ dsilu_z = sig_z * (1.0 + z_val * (1.0 - sig_z))
126
+
127
+ y = tl.sum(tl.where(mask_n, h_t * C_vec, 0.0), axis=0)
128
+ core = y + x_val * D
129
+
130
+ dz_val = dout_val * core * dsilu_z
131
+ dy_val = dout_val * silu_z
132
+
133
+ tl.store(dz_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt, dz_val)
134
+ tl.store(dy_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt, dy_val)
135
+
136
+ accum_dD += dy_val * x_val
137
+
138
+ dh_t = dy_val * C_vec + lambda_h
139
+ tl.store(dh_lambda_ptr + pid_b * stride_hb + pid_d * stride_hd + t * stride_ht + offs_n * stride_hn, dh_t, mask=mask_n)
140
+
141
+ du_x_dt = tl.sum(tl.where(mask_n, dh_t * B_vec, 0.0), axis=0)
142
+ dx_val = dy_val * D + du_x_dt * dt_val
143
+ tl.store(dx_ptr + pid_b * stride_xb + pid_d * stride_xd + t * stride_xt, dx_val)
144
+
145
+ dA_step = tl.exp(dt_val * A)
146
+ term_A = tl.sum(tl.where(mask_n, dh_t * h_prev * A * dA_step, 0.0), axis=0)
147
+ ddt_val = du_x_dt * x_val + term_A
148
+ tl.store(ddt_ptr + pid_b * stride_dtb + pid_d * stride_dtd + t * stride_dtt, ddt_val)
149
+
150
+ accum_dA += tl.where(mask_n, dh_t * h_prev * dt_val * dA_step, 0.0)
151
+ lambda_h = dh_t * dA_step
152
+
153
+ tl.atomic_add(dA_ptr + pid_d * stride_Ad + offs_n * stride_An, accum_dA, mask=mask_n)
154
+ tl.atomic_add(dD_ptr + pid_d, accum_dD)
155
+
156
+
157
+ class TritonSSMFunction(torch.autograd.Function):
158
+ @staticmethod
159
+ def forward(ctx, x, dt, A, B_ssm, C_ssm, D_skip, z):
160
+ B_sz, d_in, T_sz = x.shape
161
+ d_st = A.shape[1]
162
+ out = torch.empty_like(x)
163
+ h_all = torch.empty(B_sz, d_in, T_sz, d_st, device=x.device, dtype=torch.bfloat16)
164
+
165
+ grid = (B_sz, d_in)
166
+ ssm_fwd_kernel[grid](
167
+ x, dt, A, B_ssm, C_ssm, D_skip, z, out, h_all,
168
+ x.stride(0), x.stride(1), x.stride(2),
169
+ dt.stride(0), dt.stride(1), dt.stride(2),
170
+ A.stride(0), A.stride(1),
171
+ B_ssm.stride(0), B_ssm.stride(1), B_ssm.stride(2),
172
+ C_ssm.stride(0), C_ssm.stride(1), C_ssm.stride(2),
173
+ out.stride(0), out.stride(1), out.stride(2),
174
+ h_all.stride(0), h_all.stride(1), h_all.stride(2), h_all.stride(3),
175
+ T=T_sz, N=d_st, N_BLOCK=64,
176
+ )
177
+ ctx.save_for_backward(x, dt, A, B_ssm, C_ssm, D_skip, z, h_all)
178
+ return out
179
+
180
+ @staticmethod
181
+ def backward(ctx, dout):
182
+ x, dt, A, B_ssm, C_ssm, D_skip, z, h_all = ctx.saved_tensors
183
+ B_sz, d_in, T_sz = x.shape
184
+ d_st = A.shape[1]
185
+
186
+ dx = torch.empty_like(x)
187
+ ddt = torch.empty_like(dt)
188
+ dz = torch.empty_like(z)
189
+ dy = torch.empty_like(x)
190
+ dh_lambda = torch.empty_like(h_all)
191
+ dA = torch.zeros_like(A, dtype=torch.float32)
192
+ dD = torch.zeros_like(D_skip, dtype=torch.float32)
193
+
194
+ grid = (B_sz, d_in)
195
+ ssm_bwd_fast_kernel[grid](
196
+ dout, x, dt, A, B_ssm, C_ssm, D_skip, z, h_all,
197
+ dx, ddt, dA, dD, dz, dy, dh_lambda,
198
+ x.stride(0), x.stride(1), x.stride(2),
199
+ dt.stride(0), dt.stride(1), dt.stride(2),
200
+ A.stride(0), A.stride(1),
201
+ B_ssm.stride(0), B_ssm.stride(1), B_ssm.stride(2),
202
+ C_ssm.stride(0), C_ssm.stride(1), C_ssm.stride(2),
203
+ dout.stride(0), dout.stride(1), dout.stride(2),
204
+ h_all.stride(0), h_all.stride(1), h_all.stride(2), h_all.stride(3),
205
+ T=T_sz, N=d_st, N_BLOCK=64,
206
+ )
207
+
208
+ # Zero atomic collisions: fast vectorized contractions
209
+ dC = torch.einsum('bdt, bdtn -> bnt', dy.float(), h_all.float()).to(C_ssm.dtype)
210
+ dt_x = (dt * x).float()
211
+ dB = torch.einsum('bdt, bdtn -> bnt', dt_x, dh_lambda.float()).to(B_ssm.dtype)
212
+
213
+ return (
214
+ dx,
215
+ ddt,
216
+ dA.to(A.dtype),
217
+ dB,
218
+ dC,
219
+ dD.to(D_skip.dtype),
220
+ dz,
221
+ )
222
+
223
+
224
+ def fused_triton_ssm(
225
+ x: torch.Tensor,
226
+ dt: torch.Tensor,
227
+ A: torch.Tensor,
228
+ B_ssm: torch.Tensor,
229
+ C_ssm: torch.Tensor,
230
+ D_skip: torch.Tensor,
231
+ z: torch.Tensor,
232
+ ) -> torch.Tensor:
233
+ """
234
+ Fused selective scan forward and backward in pure Triton.
235
+ Inputs:
236
+ x: (B, d_inner, T)
237
+ dt: (B, d_inner, T)
238
+ A: (d_inner, d_state)
239
+ B_ssm: (B, d_state, T)
240
+ C_ssm: (B, d_state, T)
241
+ D_skip: (d_inner,)
242
+ z: (B, d_inner, T)
243
+ Returns:
244
+ out: (B, d_inner, T)
245
+ """
246
+ return TritonSSMFunction.apply(
247
+ x.contiguous(),
248
+ dt.contiguous(),
249
+ A.contiguous(),
250
+ B_ssm.contiguous(),
251
+ C_ssm.contiguous(),
252
+ D_skip.contiguous(),
253
+ z.contiguous(),
254
+ )
model_bf16.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5944951cd5017a98c1b26485607fac1573a14db9f218357702caeaf3b218ff6
3
+ size 1422503437
model_config.json ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab_size": 206464,
3
+ "d_model": 1024,
4
+ "n_layers": 18,
5
+ "max_seq_len": 4096,
6
+ "mha_layer_indices": [
7
+ 5,
8
+ 11,
9
+ 17
10
+ ],
11
+ "dense_ffn_layer_indices": [
12
+ 1,
13
+ 3,
14
+ 5,
15
+ 7,
16
+ 9,
17
+ 11,
18
+ 13,
19
+ 15,
20
+ 17
21
+ ],
22
+ "hse_ffn_layer_indices": [
23
+ 0,
24
+ 2,
25
+ 4,
26
+ 6,
27
+ 8,
28
+ 10,
29
+ 12,
30
+ 14,
31
+ 16
32
+ ],
33
+ "arg_d_inner": 1024,
34
+ "arg_d_state": 48,
35
+ "arg_d_rank": 48,
36
+ "arg_conv_kernel": 4,
37
+ "arg_local_window": 64,
38
+ "arg_local_n_heads": 8,
39
+ "arg_local_n_kv_heads": 2,
40
+ "arg_head_dim": 64,
41
+ "arg_scalar_gate": true,
42
+ "mha_n_heads": 16,
43
+ "mha_n_kv_heads": 4,
44
+ "mha_head_dim": 64,
45
+ "rope_theta": 500000.0,
46
+ "rope_scaling_type": null,
47
+ "rope_scaling_factor": 1.0,
48
+ "dense_d_ff": 2560,
49
+ "hse_n_domains": 4,
50
+ "hse_n_experts_per_domain": 6,
51
+ "hse_top_k": 2,
52
+ "hse_d_ff_expert": 210,
53
+ "hse_capacity_factor": 1.25,
54
+ "hse_expert_dropout": 0.1,
55
+ "hse_aux_loss_alpha": 0.01,
56
+ "hse_gumbel_tau_init": 1.0,
57
+ "hse_gumbel_tau_final": 0.1,
58
+ "hse_gumbel_tau_warmup_tokens": 10000000000,
59
+ "norm_type": "dgn",
60
+ "dgn_n_groups": 16,
61
+ "norm_eps": 1e-06,
62
+ "init_std": 0.02,
63
+ "init_std_residual_scale": true,
64
+ "dropout": 0.0,
65
+ "tie_word_embeddings": true,
66
+ "use_cache": true,
67
+ "pad_token_id": 0,
68
+ "bos_token_id": 1,
69
+ "eos_token_id": 2,
70
+ "use_flash_attention": true,
71
+ "use_triton_kernels": true,
72
+ "use_torch_compile": true,
73
+ "compile_mode": "max-autotune",
74
+ "use_gradient_checkpointing": true,
75
+ "gradient_checkpointing_ratio": 1.0
76
+ }
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FORGE-3B dependencies
2
+ torch>=2.3.0
3
+ transformers>=4.41.0
4
+ datasets>=2.19.0
5
+ tokenizers>=0.19.1
6
+ wandb==0.17.0
7
+ deepspeed>=0.14.4
8
+ flash-attn>=2.5.8
9
+ einops==0.8.0
10
+ accelerate==0.30.1
11
+ bitsandbytes==0.43.1
12
+ mamba-ssm>=1.2.0
13
+ causal-conv1d>=1.3.0
14
+ numpy>=1.24.0
15
+ triton>=2.1.0
16
+ xerv-crayon
run_inference.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import argparse
4
+ import logging
5
+ import torch
6
+ from pathlib import Path
7
+
8
+ # Set up logging
9
+ logging.basicConfig(level=logging.INFO, format="[%(asctime)s][%(levelname)s] %(message)s")
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def get_args():
13
+ parser = argparse.ArgumentParser(description="Run inference on FORGE-500M using a trained model checkpoint")
14
+ parser.add_argument("--model_path", type=str, default=".",
15
+ help="Path to the trained model directory containing config, tokenizer, and weights (defaults to current directory)")
16
+ parser.add_argument("--prompt", type=str, default="The future of artificial intelligence is",
17
+ help="Prompt to generate text from")
18
+ parser.add_argument("--max_new_tokens", type=int, default=128,
19
+ help="Max number of tokens to generate")
20
+ parser.add_argument("--temperature", type=float, default=0.7,
21
+ help="Sampling temperature for text generation")
22
+ parser.add_argument("--top_k", type=int, default=50,
23
+ help="Top-k sampling parameter")
24
+ parser.add_argument("--top_p", type=float, default=0.9,
25
+ help="Top-p sampling parameter")
26
+ parser.add_argument("--repetition_penalty", type=float, default=1.1,
27
+ help="Repetition penalty parameter")
28
+ return parser.parse_args()
29
+
30
+ def main():
31
+ args = get_args()
32
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
+ logger.info(f"Using device: {device}")
34
+
35
+ model_dir = Path(args.model_path)
36
+ if not model_dir.exists():
37
+ raise FileNotFoundError(f"Model path does not exist: {model_dir}")
38
+
39
+ # 1. Load Tokenizer
40
+ from tokenizer.crayon_wrapper import ForgeTokenizer
41
+ tok_dir = model_dir / "tokenizer"
42
+ if tok_dir.exists():
43
+ logger.info(f"Loading tokenizer from {tok_dir}...")
44
+ tokenizer = ForgeTokenizer.from_pretrained(str(tok_dir))
45
+ else:
46
+ logger.info(f"Loading tokenizer from model directory root {model_dir}...")
47
+ tokenizer = ForgeTokenizer.from_pretrained(str(model_dir))
48
+
49
+ # 2. Load Model Config
50
+ from config import ForgeModelConfig
51
+ config_path = model_dir / "model_config.json"
52
+ if not config_path.exists():
53
+ config_path = model_dir / "config.json"
54
+ if not config_path.exists():
55
+ raise FileNotFoundError(f"Could not find model_config.json or config.json in {model_dir}")
56
+
57
+ logger.info(f"Loading config from {config_path}")
58
+ model_config = ForgeModelConfig.from_json(str(config_path))
59
+ model_config.vocab_size = tokenizer.vocab_size
60
+
61
+ # 3. Build Model
62
+ from model.forge_model import build_forge_500m
63
+ logger.info("Building FORGE model...")
64
+ model = build_forge_500m(model_config)
65
+
66
+ # 4. Load Model Weights
67
+ loaded_weights = False
68
+ for weight_filename in ("model_bf16.pt", "model.pt", "pytorch_model.bin"):
69
+ weight_path = model_dir / weight_filename
70
+ if weight_path.exists():
71
+ logger.info(f"Loading model weights from {weight_path}...")
72
+ state_dict = torch.load(str(weight_path), map_location="cpu")
73
+ # Convert values to bfloat16
74
+ state_dict = {k: v.to(torch.bfloat16) for k, v in state_dict.items()}
75
+ model.load_state_dict(state_dict, strict=False)
76
+ del state_dict
77
+ torch.cuda.empty_cache()
78
+ loaded_weights = True
79
+ break
80
+
81
+ if not loaded_weights:
82
+ raise FileNotFoundError(
83
+ f"No model weights found in {model_dir}. "
84
+ "Expected one of: model_bf16.pt, model.pt, pytorch_model.bin"
85
+ )
86
+
87
+ model = model.to(device).to(torch.bfloat16)
88
+ model.eval()
89
+
90
+ # 5. Encode prompt
91
+ logger.info(f"Prompt: {args.prompt!r}")
92
+ input_ids = torch.tensor([tokenizer.encode(args.prompt, add_bos=True, add_eos=False)], dtype=torch.long, device=device)
93
+
94
+ # 6. Generate
95
+ logger.info("Generating tokens...")
96
+ with torch.inference_mode():
97
+ output_ids = model.generate(
98
+ input_ids=input_ids,
99
+ max_new_tokens=args.max_new_tokens,
100
+ temperature=args.temperature,
101
+ top_k=args.top_k,
102
+ top_p=args.top_p,
103
+ repetition_penalty=args.repetition_penalty,
104
+ eos_token_id=tokenizer.special_tokens["<|eos|>"],
105
+ pad_token_id=tokenizer.special_tokens["<|pad|>"]
106
+ )
107
+
108
+ # 7. Decode output
109
+ generated_text = tokenizer.decode(output_ids[0].tolist(), skip_special_tokens=False)
110
+ logger.info(f"Generated text:\n{generated_text}")
111
+
112
+ if __name__ == "__main__":
113
+ main()
tokenizer/crayon_wrapper.py ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CRAYON Tokenizer Wrapper
3
+ Wraps CrayonVocab into a full-featured tokenizer compatible with FORGE training.
4
+ Exploits CRAYON's 24M+ tok/sec CPU throughput via parallel multi-process pipeline.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import gc
11
+ import math
12
+ import time
13
+ import logging
14
+ import threading
15
+ import multiprocessing as mp
16
+ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
17
+ from typing import List, Union, Optional, Dict, Any, Iterator, Tuple
18
+ from pathlib import Path
19
+ import queue
20
+
21
+ # Fix Windows console UTF-8 printing issues for external packages (e.g., Crayon print statements with emoji)
22
+ import sys
23
+ import io
24
+ if sys.platform.startswith("win"):
25
+ try:
26
+ sys.stdout.reconfigure(encoding='utf-8')
27
+ sys.stderr.reconfigure(encoding='utf-8')
28
+ except AttributeError:
29
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
30
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
31
+
32
+ import torch
33
+ import numpy as np
34
+
35
+ from crayon import CrayonVocab
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ # THREAD-LOCAL TOKENIZER POOL
42
+ # Each worker thread gets its own CrayonVocab instance to avoid any GIL
43
+ # contention on the underlying C++ DAT engine.
44
+ # ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ _thread_local = threading.local()
47
+ _init_lock = threading.Lock()
48
+
49
+ def _get_thread_local_tokenizer(profile: str = "standard") -> CrayonVocab:
50
+ """Return a thread-local CrayonVocab instance, creating one if needed."""
51
+ if not hasattr(_thread_local, "tokenizer"):
52
+ with _init_lock:
53
+ # CPU is 20Γ— faster than CUDA for CRAYON (23.84M vs 1.22M tok/s)
54
+ _thread_local.tokenizer = CrayonVocab(device="cpu")
55
+ _thread_local.tokenizer.load_profile(profile)
56
+ logger.debug(f"Thread {threading.current_thread().name}: "
57
+ f"CrayonVocab loaded with profile='{profile}'")
58
+ return _thread_local.tokenizer
59
+
60
+
61
+ # ─────────────────────────────────────────────────────────────────────────────
62
+ # SPECIAL TOKEN REGISTRY
63
+ # We manage special tokens separately since CRAYON's vocabulary may not have
64
+ # them natively β€” we append them to the vocab ID space.
65
+ # ─────────────────────────────────────────────────────────────────────────────
66
+
67
+ SPECIAL_TOKENS = {
68
+ "<|pad|>": 0,
69
+ "<|bos|>": 1,
70
+ "<|eos|>": 2,
71
+ "<|sep|>": 3,
72
+ "<|sys|>": 4,
73
+ "<|/sys|>": 5,
74
+ "<|usr|>": 6,
75
+ "<|/usr|>": 7,
76
+ "<|asst|>": 8,
77
+ "<|/asst|>": 9,
78
+ "<|tool|>": 10,
79
+ "<|tool_resp|>": 11,
80
+ "<|unk|>": 12,
81
+ }
82
+
83
+ # For the standard profile: actual vocab starts at ID 13
84
+ # Effective vocab_size = 206,373 (crayon) + 13 (specials) = 206,386 β†’ pad to 206,464
85
+ SPECIAL_TOKEN_OFFSET = len(SPECIAL_TOKENS) # 13
86
+ CRAYON_STANDARD_VOCAB = 206_373
87
+ CRAYON_LITE_VOCAB = 50_000
88
+
89
+ # Reverse mapping for decode
90
+ ID_TO_SPECIAL = {v: k for k, v in SPECIAL_TOKENS.items()}
91
+
92
+
93
+ class ForgeTokenizer:
94
+ """
95
+ Production-grade FORGE tokenizer built on top of CRAYON.
96
+
97
+ Features:
98
+ - Thread-safe, zero-copy multi-threaded tokenization
99
+ - Batched encoding with automatic chunking for throughput
100
+ - Full special token support with chat template
101
+ - Streaming decode
102
+ - Vocabulary size adapter between CRAYON profile and model vocab
103
+ - Offline preprocessing at 24M+ tokens/sec on CPU
104
+
105
+ Usage:
106
+ tokenizer = ForgeTokenizer(profile="standard")
107
+ ids = tokenizer.encode("Hello world")
108
+ text = tokenizer.decode(ids)
109
+
110
+ # Chat format
111
+ ids = tokenizer.encode_chat([
112
+ {"role": "system", "content": "You are helpful."},
113
+ {"role": "user", "content": "What is 2+2?"},
114
+ ])
115
+ """
116
+
117
+ def __init__(
118
+ self,
119
+ profile: str = "standard",
120
+ device: str = "cpu",
121
+ n_workers: int = None,
122
+ max_length: Optional[int] = None,
123
+ add_bos: bool = True,
124
+ add_eos: bool = True,
125
+ ):
126
+ self.profile = profile
127
+ self.device_str = device # "cpu" recommended for CRAYON throughput
128
+ self.n_workers = n_workers or max(1, mp.cpu_count() - 2)
129
+ self.max_length = max_length
130
+ self.add_bos = add_bos
131
+ self.add_eos = add_eos
132
+
133
+ # Primary tokenizer (used for single-threaded ops)
134
+ self._tokenizer = CrayonVocab(device=device)
135
+ self._tokenizer.load_profile(profile)
136
+
137
+ # Special tokens
138
+ self.special_tokens = SPECIAL_TOKENS.copy()
139
+ self.id_to_special = ID_TO_SPECIAL.copy()
140
+ self.special_token_offset = SPECIAL_TOKEN_OFFSET
141
+
142
+ # Derive vocab size
143
+ if profile == "standard":
144
+ self._crayon_vocab_size = CRAYON_STANDARD_VOCAB
145
+ else:
146
+ self._crayon_vocab_size = CRAYON_LITE_VOCAB
147
+
148
+ # Effective vocab size = specials + crayon tokens
149
+ # Padded to nearest multiple of 128 for efficient matmuls
150
+ raw_vocab = self._crayon_vocab_size + SPECIAL_TOKEN_OFFSET
151
+ self.vocab_size = math.ceil(raw_vocab / 128) * 128
152
+
153
+ # Lock for thread safety on single-instance operations
154
+ self._lock = threading.Lock()
155
+
156
+ # Thread pool for parallel tokenization
157
+ self._thread_pool = ThreadPoolExecutor(
158
+ max_workers=self.n_workers,
159
+ thread_name_prefix="forge_tok"
160
+ )
161
+
162
+ logger.info(
163
+ f"ForgeTokenizer initialized: profile={profile}, "
164
+ f"vocab_size={self.vocab_size} (crayon={self._crayon_vocab_size} "
165
+ f"+ specials={SPECIAL_TOKEN_OFFSET} + padding={self.vocab_size - raw_vocab}), "
166
+ f"n_workers={self.n_workers}, throughput ~24M tok/s (CPU)"
167
+ )
168
+
169
+ # ─────────────────────────────────────────────────────────────────────────
170
+ # TOKEN ID CONVERSION HELPERS
171
+ # CRAYON returns IDs in [0, crayon_vocab_size).
172
+ # We shift them by SPECIAL_TOKEN_OFFSET to [SPECIAL_TOKEN_OFFSET, vocab_size).
173
+ # Special tokens occupy IDs [0, SPECIAL_TOKEN_OFFSET).
174
+ # ─────────────────────────────────────────────────────────────────────────
175
+
176
+ def _shift_crayon_ids(self, ids: List[int]) -> List[int]:
177
+ """Shift CRAYON token IDs to make room for special tokens at the front."""
178
+ return [i + self.special_token_offset for i in ids]
179
+
180
+ def _unshift_crayon_ids(self, ids: List[int]) -> List[int]:
181
+ """Inverse of _shift_crayon_ids for decode path."""
182
+ out = []
183
+ for i in ids:
184
+ if i < self.special_token_offset:
185
+ out.append(None) # special token β€” handled separately
186
+ else:
187
+ out.append(i - self.special_token_offset)
188
+ return out
189
+
190
+ # ─────────────────────────────────────────────────────────────────────────
191
+ # CORE ENCODE / DECODE
192
+ # ─────────────────────────────────────────────────────────────────────────
193
+
194
+ def encode(
195
+ self,
196
+ text: str,
197
+ add_bos: Optional[bool] = None,
198
+ add_eos: Optional[bool] = None,
199
+ truncate: bool = True,
200
+ ) -> List[int]:
201
+ """
202
+ Encode a single string to token IDs.
203
+ Thread-safe via CRAYON's internal lock or thread-local tokenizers.
204
+ """
205
+ add_bos_ = self.add_bos if add_bos is None else add_bos
206
+ add_eos_ = self.add_eos if add_eos is None else add_eos
207
+
208
+ # CRAYON tokenization
209
+ with self._lock:
210
+ raw_ids = self._tokenizer.tokenize(text)
211
+
212
+ # Handle both list and tensor returns from CRAYON
213
+ if isinstance(raw_ids, torch.Tensor):
214
+ raw_ids = raw_ids.tolist()
215
+
216
+ ids = self._shift_crayon_ids(raw_ids)
217
+
218
+ # Prepend/append special tokens
219
+ if add_bos_:
220
+ ids = [self.special_tokens["<|bos|>"]] + ids
221
+ if add_eos_:
222
+ ids = ids + [self.special_tokens["<|eos|>"]]
223
+
224
+ # Truncate if needed
225
+ if truncate and self.max_length is not None:
226
+ ids = ids[:self.max_length]
227
+
228
+ return ids
229
+
230
+ def encode_batch(
231
+ self,
232
+ texts: List[str],
233
+ add_bos: Optional[bool] = None,
234
+ add_eos: Optional[bool] = None,
235
+ pad: bool = True,
236
+ pad_to_multiple_of: int = 8,
237
+ return_tensors: str = "pt",
238
+ ) -> Dict[str, torch.Tensor]:
239
+ """
240
+ Encode a batch of texts in parallel using thread pool.
241
+ Leverages CRAYON's 24M tok/s CPU throughput across all threads.
242
+
243
+ Returns dict with 'input_ids' and 'attention_mask'.
244
+ """
245
+ add_bos_ = self.add_bos if add_bos is None else add_bos
246
+ add_eos_ = self.add_eos if add_eos is None else add_eos
247
+
248
+ # ── Parallel tokenization ──────────────────────────────────────────
249
+ def _tokenize_one(text: str) -> List[int]:
250
+ tok = _get_thread_local_tokenizer(self.profile)
251
+ raw = tok.tokenize(text)
252
+ if isinstance(raw, torch.Tensor):
253
+ raw = raw.tolist()
254
+ ids = self._shift_crayon_ids(raw)
255
+ if add_bos_:
256
+ ids = [self.special_tokens["<|bos|>"]] + ids
257
+ if add_eos_:
258
+ ids = ids + [self.special_tokens["<|eos|>"]]
259
+ if self.max_length is not None:
260
+ ids = ids[:self.max_length]
261
+ return ids
262
+
263
+ if self.n_workers <= 1:
264
+ # Run sequentially on the main thread using the primary tokenizer instance
265
+ # to completely avoid any C++ multi-threading deadlocks.
266
+ all_ids = []
267
+ for t in texts:
268
+ raw = self._tokenizer.tokenize(t)
269
+ if isinstance(raw, torch.Tensor):
270
+ raw = raw.tolist()
271
+ ids = self._shift_crayon_ids(raw)
272
+ if add_bos_:
273
+ ids = [self.special_tokens["<|bos|>"]] + ids
274
+ if add_eos_:
275
+ ids = ids + [self.special_tokens["<|eos|>"]]
276
+ if self.max_length is not None:
277
+ ids = ids[:self.max_length]
278
+ all_ids.append(ids)
279
+ else:
280
+ # Submit all texts to thread pool
281
+ futures = [self._thread_pool.submit(_tokenize_one, t) for t in texts]
282
+ all_ids = [f.result() for f in futures]
283
+
284
+ if not pad or return_tensors is None:
285
+ return {"input_ids": all_ids}
286
+
287
+ # ── Padding ────────────────────────────────────────────────────────
288
+ max_len = max(len(ids) for ids in all_ids)
289
+ # Pad to multiple for efficiency
290
+ if pad_to_multiple_of > 1:
291
+ max_len = math.ceil(max_len / pad_to_multiple_of) * pad_to_multiple_of
292
+
293
+ pad_id = self.special_tokens["<|pad|>"]
294
+ padded_ids = []
295
+ attention_masks = []
296
+
297
+ for ids in all_ids:
298
+ n_pad = max_len - len(ids)
299
+ padded_ids.append(ids + [pad_id] * n_pad)
300
+ attention_masks.append([1] * len(ids) + [0] * n_pad)
301
+
302
+ if return_tensors == "pt":
303
+ return {
304
+ "input_ids": torch.tensor(padded_ids, dtype=torch.long),
305
+ "attention_mask": torch.tensor(attention_masks, dtype=torch.long),
306
+ }
307
+ return {"input_ids": padded_ids, "attention_mask": attention_masks}
308
+
309
+ def decode(
310
+ self,
311
+ ids: Union[List[int], torch.Tensor],
312
+ skip_special_tokens: bool = True,
313
+ ) -> str:
314
+ """Decode token IDs back to text."""
315
+ if isinstance(ids, torch.Tensor):
316
+ ids = ids.tolist()
317
+
318
+ # Separate special tokens from CRAYON tokens
319
+ crayon_ids = []
320
+ special_strs = []
321
+
322
+ for tok_id in ids:
323
+ if tok_id < self.special_token_offset:
324
+ if not skip_special_tokens and tok_id in self.id_to_special:
325
+ special_strs.append(self.id_to_special[tok_id])
326
+ else:
327
+ crayon_ids.append(tok_id - self.special_token_offset)
328
+
329
+ # CRAYON decode
330
+ if crayon_ids:
331
+ with self._lock:
332
+ text = self._tokenizer.decode(crayon_ids)
333
+ else:
334
+ text = ""
335
+
336
+ # Re-insert special tokens if not skipping
337
+ if special_strs and not skip_special_tokens:
338
+ text = " ".join(special_strs) + " " + text
339
+
340
+ return text
341
+
342
+ def decode_batch(
343
+ self,
344
+ batch_ids: Union[List[List[int]], torch.Tensor],
345
+ skip_special_tokens: bool = True,
346
+ ) -> List[str]:
347
+ """Decode a batch in parallel."""
348
+ if isinstance(batch_ids, torch.Tensor):
349
+ batch_ids = batch_ids.tolist()
350
+
351
+ if self.n_workers <= 1:
352
+ return [self.decode(ids, skip_special_tokens) for ids in batch_ids]
353
+
354
+ futures = [
355
+ self._thread_pool.submit(self.decode, ids, skip_special_tokens)
356
+ for ids in batch_ids
357
+ ]
358
+ return [f.result() for f in futures]
359
+
360
+ # ─────────────────────────────────────────────────────────────────────────
361
+ # CHAT TEMPLATE
362
+ # ──────────────────────────────────────────────────────────────────���──────
363
+
364
+ def encode_chat(
365
+ self,
366
+ messages: List[Dict[str, str]],
367
+ add_generation_prompt: bool = False,
368
+ return_as_tensor: bool = True,
369
+ ) -> Union[List[int], torch.Tensor]:
370
+ """
371
+ Encode a conversation using FORGE chat template.
372
+
373
+ Template:
374
+ <|bos|><|sys|>{system}<|/sys|>
375
+ <|usr|>{user}<|/usr|>
376
+ <|asst|>{assistant}<|/asst|>
377
+ ...
378
+
379
+ Args:
380
+ messages: List of {"role": "system|user|assistant", "content": str}
381
+ add_generation_prompt: If True, append <|asst|> at end (inference)
382
+ """
383
+ token_ids = [self.special_tokens["<|bos|>"]]
384
+
385
+ role_to_open = {
386
+ "system": "<|sys|>",
387
+ "user": "<|usr|>",
388
+ "assistant": "<|asst|>",
389
+ }
390
+ role_to_close = {
391
+ "system": "<|/sys|>",
392
+ "user": "<|/usr|>",
393
+ "assistant": "<|/asst|>",
394
+ }
395
+
396
+ for msg in messages:
397
+ role = msg["role"]
398
+ content = msg["content"]
399
+
400
+ # Open tag
401
+ open_tag = role_to_open.get(role, "<|usr|>")
402
+ close_tag = role_to_close.get(role, "<|/usr|>")
403
+
404
+ token_ids.append(self.special_tokens.get(open_tag, self.special_tokens["<|unk|>"]))
405
+
406
+ # Encode content (no bos/eos, no truncation)
407
+ content_ids = self.encode(content, add_bos=False, add_eos=False, truncate=False)
408
+ token_ids.extend(content_ids)
409
+
410
+ token_ids.append(self.special_tokens.get(close_tag, self.special_tokens["<|unk|>"]))
411
+
412
+ if add_generation_prompt:
413
+ token_ids.append(self.special_tokens["<|asst|>"])
414
+
415
+ if self.max_length is not None:
416
+ token_ids = token_ids[:self.max_length]
417
+
418
+ if return_as_tensor:
419
+ return torch.tensor(token_ids, dtype=torch.long)
420
+ return token_ids
421
+
422
+ def encode_chat_with_labels(
423
+ self,
424
+ messages: List[Dict[str, str]],
425
+ loss_on_prompt: bool = False,
426
+ ) -> Dict[str, torch.Tensor]:
427
+ """
428
+ Encode a conversation and produce labels for SFT.
429
+ Labels are -100 (ignored) for non-assistant turns unless loss_on_prompt=True.
430
+
431
+ Returns dict with 'input_ids' and 'labels'.
432
+ """
433
+ input_ids = []
434
+ labels = []
435
+
436
+ input_ids.append(self.special_tokens["<|bos|>"])
437
+ labels.append(-100 if not loss_on_prompt else self.special_tokens["<|bos|>"])
438
+
439
+ role_to_open = {
440
+ "system": "<|sys|>",
441
+ "user": "<|usr|>",
442
+ "assistant": "<|asst|>",
443
+ }
444
+ role_to_close = {
445
+ "system": "<|/sys|>",
446
+ "user": "<|/usr|>",
447
+ "assistant": "<|/asst|>",
448
+ }
449
+
450
+ for msg in messages:
451
+ role = msg["role"]
452
+ content = msg["content"]
453
+ is_assistant = (role == "assistant")
454
+
455
+ open_tag_id = self.special_tokens.get(role_to_open.get(role, "<|usr|>"),
456
+ self.special_tokens["<|unk|>"])
457
+ close_tag_id = self.special_tokens.get(role_to_close.get(role, "<|/usr|>"),
458
+ self.special_tokens["<|unk|>"])
459
+
460
+ content_ids = self.encode(content, add_bos=False, add_eos=False, truncate=False)
461
+
462
+ turn_ids = [open_tag_id] + content_ids + [close_tag_id]
463
+ input_ids.extend(turn_ids)
464
+
465
+ if is_assistant or loss_on_prompt:
466
+ # Compute loss on assistant content + close tag, NOT on open tag
467
+ turn_labels = [-100] + content_ids + [close_tag_id]
468
+ labels.extend(turn_labels)
469
+ else:
470
+ labels.extend([-100] * len(turn_ids))
471
+
472
+ if self.max_length is not None:
473
+ input_ids = input_ids[:self.max_length]
474
+ labels = labels[:self.max_length]
475
+
476
+ return {
477
+ "input_ids": torch.tensor(input_ids, dtype=torch.long),
478
+ "labels": torch.tensor(labels, dtype=torch.long),
479
+ }
480
+
481
+ # ─────────────────────────────────────────────────────────────────────────
482
+ # HIGH-THROUGHPUT OFFLINE PREPROCESSING
483
+ # ─────────────────────────────────────────────────────────────────────────
484
+
485
+ def tokenize_file_streaming(
486
+ self,
487
+ input_path: str,
488
+ output_path: str,
489
+ chunk_size: int = 100_000, # documents per chunk
490
+ seq_len: int = 2048,
491
+ append_eos: bool = True,
492
+ n_processes: int = None,
493
+ ) -> int:
494
+ """
495
+ High-throughput offline tokenization of a large text file.
496
+
497
+ Architecture:
498
+ - Producer process reads text lines
499
+ - N worker processes each run their own CrayonVocab instance
500
+ - Consumer aggregates and packs sequences
501
+
502
+ Throughput: ~5-10GB/min depending on hardware.
503
+ Returns: total number of tokens written.
504
+ """
505
+ n_processes = n_processes or max(1, mp.cpu_count() - 2)
506
+ total_tokens = 0
507
+
508
+ logger.info(f"Offline tokenization: {input_path} β†’ {output_path}, "
509
+ f"n_processes={n_processes}, seq_len={seq_len}")
510
+
511
+ # We use process-based parallelism to bypass GIL fully
512
+ with ProcessPoolExecutor(max_workers=n_processes) as pool:
513
+ with open(input_path, "r", encoding="utf-8", errors="replace") as fin:
514
+ chunk_buffer = []
515
+ futures = []
516
+
517
+ for line in fin:
518
+ chunk_buffer.append(line.strip())
519
+ if len(chunk_buffer) >= chunk_size:
520
+ futures.append(
521
+ pool.submit(
522
+ _tokenize_chunk_worker,
523
+ chunk_buffer.copy(),
524
+ self.profile,
525
+ self.special_token_offset,
526
+ SPECIAL_TOKENS["<|eos|>"] if append_eos else None,
527
+ )
528
+ )
529
+ chunk_buffer.clear()
530
+
531
+ # Submit remaining
532
+ if chunk_buffer:
533
+ futures.append(
534
+ pool.submit(
535
+ _tokenize_chunk_worker,
536
+ chunk_buffer,
537
+ self.profile,
538
+ self.special_token_offset,
539
+ SPECIAL_TOKENS["<|eos|>"] if append_eos else None,
540
+ )
541
+ )
542
+
543
+ # Collect and pack
544
+ token_stream = []
545
+ packed_sequences = []
546
+
547
+ for future in as_completed(futures):
548
+ chunk_tokens = future.result() # List[int]
549
+ token_stream.extend(chunk_tokens)
550
+
551
+ # Pack into fixed-length sequences
552
+ while len(token_stream) >= seq_len:
553
+ packed_sequences.append(token_stream[:seq_len])
554
+ token_stream = token_stream[seq_len:]
555
+ total_tokens += seq_len
556
+
557
+ # Flush to disk in chunks
558
+ if len(packed_sequences) >= 10_000:
559
+ _flush_to_disk(packed_sequences, output_path, total_tokens)
560
+ packed_sequences.clear()
561
+
562
+ # Flush remaining
563
+ if packed_sequences:
564
+ _flush_to_disk(packed_sequences, output_path, total_tokens)
565
+
566
+ logger.info(f"Tokenization complete: {total_tokens:,} tokens written to {output_path}")
567
+ return total_tokens
568
+
569
+ def __repr__(self) -> str:
570
+ return (f"ForgeTokenizer(profile={self.profile!r}, "
571
+ f"vocab_size={self.vocab_size:,}, "
572
+ f"n_workers={self.n_workers})")
573
+
574
+ def save_pretrained(self, path: str):
575
+ """Save tokenizer config for later loading."""
576
+ import json
577
+ os.makedirs(path, exist_ok=True)
578
+ config = {
579
+ "profile": self.profile,
580
+ "device": self.device_str,
581
+ "vocab_size": self.vocab_size,
582
+ "special_tokens": self.special_tokens,
583
+ "special_token_offset": self.special_token_offset,
584
+ "add_bos": self.add_bos,
585
+ "add_eos": self.add_eos,
586
+ "max_length": self.max_length,
587
+ }
588
+ with open(os.path.join(path, "tokenizer_config.json"), "w") as f:
589
+ json.dump(config, f, indent=2)
590
+ logger.info(f"Tokenizer config saved to {path}")
591
+
592
+ @classmethod
593
+ def from_pretrained(cls, path: str) -> "ForgeTokenizer":
594
+ import json
595
+ with open(os.path.join(path, "tokenizer_config.json")) as f:
596
+ config = json.load(f)
597
+ return cls(
598
+ profile=config["profile"],
599
+ device=config.get("device", "cpu"),
600
+ max_length=config.get("max_length"),
601
+ add_bos=config.get("add_bos", True),
602
+ add_eos=config.get("add_eos", True),
603
+ )
604
+
605
+ def __del__(self):
606
+ """Graceful cleanup of thread pool."""
607
+ try:
608
+ self._thread_pool.shutdown(wait=False)
609
+ except Exception:
610
+ pass
611
+
612
+
613
+ # ─────────────────────────────────────────────────────────────────────────────
614
+ # WORKER FUNCTIONS (must be module-level for pickling with ProcessPoolExecutor)
615
+ # ─────────────────────────────────────────────────────────────────────────────
616
+
617
+ def _tokenize_chunk_worker(
618
+ texts: List[str],
619
+ profile: str,
620
+ offset: int,
621
+ eos_id: Optional[int],
622
+ ) -> List[int]:
623
+ """
624
+ Worker function for parallel tokenization.
625
+ Runs in a separate process with its own CrayonVocab instance.
626
+ """
627
+ tok = CrayonVocab(device="cpu")
628
+ tok.load_profile(profile)
629
+
630
+ all_ids = []
631
+ for text in texts:
632
+ if not text:
633
+ continue
634
+ raw = tok.tokenize(text)
635
+ if isinstance(raw, torch.Tensor):
636
+ raw = raw.tolist()
637
+ ids = [i + offset for i in raw]
638
+ if eos_id is not None:
639
+ ids.append(eos_id)
640
+ all_ids.extend(ids)
641
+
642
+ return all_ids
643
+
644
+
645
+ def _flush_to_disk(sequences: List[List[int]], output_path: str, offset: int):
646
+ """Append packed sequences to a memory-mapped numpy file."""
647
+ arr = np.array(sequences, dtype=np.uint32)
648
+ mode = "ab" # append binary
649
+ with open(output_path + ".npy", mode) as f:
650
+ np.save(f, arr)
651
+ # Also write offset index for random access
652
+ with open(output_path + ".idx", "ab") as f:
653
+ np.save(f, np.array([offset], dtype=np.int64))
tokenizer/tokenizer_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "profile": "standard",
3
+ "device": "cpu",
4
+ "vocab_size": 206464,
5
+ "special_tokens": {
6
+ "<|pad|>": 0,
7
+ "<|bos|>": 1,
8
+ "<|eos|>": 2,
9
+ "<|sep|>": 3,
10
+ "<|sys|>": 4,
11
+ "<|/sys|>": 5,
12
+ "<|usr|>": 6,
13
+ "<|/usr|>": 7,
14
+ "<|asst|>": 8,
15
+ "<|/asst|>": 9,
16
+ "<|tool|>": 10,
17
+ "<|tool_resp|>": 11,
18
+ "<|unk|>": 12
19
+ },
20
+ "special_token_offset": 13,
21
+ "add_bos": true,
22
+ "add_eos": true,
23
+ "max_length": 4096
24
+ }
tokenizer_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "profile": "standard",
3
+ "device": "cpu",
4
+ "vocab_size": 206464,
5
+ "special_tokens": {
6
+ "<|pad|>": 0,
7
+ "<|bos|>": 1,
8
+ "<|eos|>": 2,
9
+ "<|sep|>": 3,
10
+ "<|sys|>": 4,
11
+ "<|/sys|>": 5,
12
+ "<|usr|>": 6,
13
+ "<|/usr|>": 7,
14
+ "<|asst|>": 8,
15
+ "<|/asst|>": 9,
16
+ "<|tool|>": 10,
17
+ "<|tool_resp|>": 11,
18
+ "<|unk|>": 12
19
+ },
20
+ "special_token_offset": 13,
21
+ "add_bos": true,
22
+ "add_eos": true,
23
+ "max_length": 4096
24
+ }