Upload folder using huggingface_hub
Browse files- README.md +205 -0
- config.py +371 -0
- model.safetensors +3 -0
- model/__init__.py +0 -0
- model/arg_layer.py +291 -0
- model/dgn_norm.py +65 -0
- model/forge_model.py +558 -0
- model/hse_layer.py +277 -0
- model/mha_layer.py +170 -0
- model/pscan.py +181 -0
- model/rotary_embedding.py +168 -0
- model/triton_kernels.py +457 -0
- model/triton_ssm.py +254 -0
- model_bf16.pt +3 -0
- model_config.json +76 -0
- requirements.txt +16 -0
- run_inference.py +113 -0
- tokenizer/crayon_wrapper.py +653 -0
- tokenizer/tokenizer_config.json +24 -0
- tokenizer_config.json +24 -0
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 |
+
[](https://opensource.org/licenses/Apache-2.0)
|
| 26 |
+
[]()
|
| 27 |
+
[]()
|
| 28 |
+
[]()
|
| 29 |
+
[]()
|
| 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 |
+
}
|