blt-trainer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blt_trainer-0.1.0/PKG-INFO +8 -0
- blt_trainer-0.1.0/README.md +0 -0
- blt_trainer-0.1.0/pyproject.toml +21 -0
- blt_trainer-0.1.0/src/blt_trainer/__init__.py +0 -0
- blt_trainer-0.1.0/src/blt_trainer/cli.py +172 -0
- blt_trainer-0.1.0/src/blt_trainer/datamix.py +88 -0
- blt_trainer-0.1.0/src/blt_trainer/model.py +258 -0
- blt_trainer-0.1.0/src/blt_trainer/optim.py +188 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: blt-trainer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure functional 96-layer Byte Latent Transformer trainer with Muon and weighted streaming datamix
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: datasets>=2.16.0
|
|
7
|
+
Requires-Dist: numpy>=1.24.0
|
|
8
|
+
Requires-Dist: torch>=2.2.0
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "blt-trainer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pure functional 96-layer Byte Latent Transformer trainer with Muon and weighted streaming datamix"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"torch>=2.2.0",
|
|
13
|
+
"datasets>=2.16.0",
|
|
14
|
+
"numpy>=1.24.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
blt-train = "blt_trainer.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/blt_trainer"]
|
|
File without changes
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn.functional as F
|
|
5
|
+
|
|
6
|
+
from blt_trainer.model import (
|
|
7
|
+
blt_local_encoder_with_surprise,
|
|
8
|
+
global_96_layer_backbone_single,
|
|
9
|
+
local_decoder_single,
|
|
10
|
+
precompute_rope_freqs,
|
|
11
|
+
)
|
|
12
|
+
from blt_trainer.optim import (
|
|
13
|
+
MuonWithScheduleSkip,
|
|
14
|
+
ScheduleFreeAdamW,
|
|
15
|
+
cut_cross_entropy,
|
|
16
|
+
partition_params_for_optimizers,
|
|
17
|
+
)
|
|
18
|
+
from blt_trainer.datamix import get_mix_loader
|
|
19
|
+
|
|
20
|
+
def parse_args():
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
description="CLI trainer for a 96-layer Byte Latent Transformer with ablation datamix."
|
|
23
|
+
)
|
|
24
|
+
# Architecture
|
|
25
|
+
parser.add_argument("--num-layers", type=int, default=96, help="Global backbone layer depth")
|
|
26
|
+
parser.add_argument("--d-global", type=int, default=288, help="Global hidden dimension")
|
|
27
|
+
parser.add_argument("--d-local", type=int, default=128, help="Local encoder/decoder dimension")
|
|
28
|
+
parser.add_argument("--num-heads", type=int, default=6, help="Backbone MHA attention heads")
|
|
29
|
+
parser.add_argument("--max-patches", type=int, default=32, help="Max static patch capacity per sequence")
|
|
30
|
+
parser.add_argument("--entropy-tau", type=float, default=3.2, help="Surprise threshold for dynamic boundaries")
|
|
31
|
+
|
|
32
|
+
# Training & Data
|
|
33
|
+
parser.add_argument("--batch-size", type=int, default=8, help="Batch size per step")
|
|
34
|
+
parser.add_argument("--block-size", type=int, default=512, help="Byte context length")
|
|
35
|
+
parser.add_argument("--lr-muon", type=float, default=0.02, help="Learning rate for Muon (2D weights)")
|
|
36
|
+
parser.add_argument("--lr-adam", type=float, default=1e-3, help="Learning rate for AdamW (1D / Embeddings)")
|
|
37
|
+
parser.add_argument("--steps", type=int, default=10000, help="Total training steps")
|
|
38
|
+
parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu")
|
|
39
|
+
parser.add_argument("--compile", action="store_true", help="Enable torch.compile kernel fusion")
|
|
40
|
+
|
|
41
|
+
return parser.parse_args()
|
|
42
|
+
|
|
43
|
+
def init_all_parameters(args, device):
|
|
44
|
+
head_dim = args.d_global // args.num_heads
|
|
45
|
+
d_mlp = (args.d_global * 8) // 3
|
|
46
|
+
|
|
47
|
+
params = {
|
|
48
|
+
# Entropy predictor
|
|
49
|
+
"entropy_embed": torch.randn(256, 64, device=device) * 0.02,
|
|
50
|
+
"entropy_head": torch.randn(256, 64, device=device) * 0.02,
|
|
51
|
+
|
|
52
|
+
# Local Encoder
|
|
53
|
+
"byte_embed": torch.randn(256, args.d_local, device=device) * 0.02,
|
|
54
|
+
"conv_w": torch.randn(args.d_local, 1, 3, device=device) * 0.02,
|
|
55
|
+
"q_seed": torch.randn(args.max_patches, args.d_global, device=device) * 0.02,
|
|
56
|
+
"W_k_enc": torch.randn(args.d_global, args.d_local, device=device) * 0.02,
|
|
57
|
+
"W_v_enc": torch.randn(args.d_global, args.d_local, device=device) * 0.02,
|
|
58
|
+
"W_out_enc": torch.randn(args.d_global, args.d_global, device=device) * 0.02,
|
|
59
|
+
|
|
60
|
+
# 96-Layer Global Backbone
|
|
61
|
+
"final_norm_w": torch.ones(args.d_global, device=device),
|
|
62
|
+
|
|
63
|
+
# Local Decoder
|
|
64
|
+
"cross_norm_w": torch.ones(args.d_local, device=device),
|
|
65
|
+
"W_q_cross": torch.randn(args.d_local, args.d_local, device=device) * 0.02,
|
|
66
|
+
"W_k_cross": torch.randn(args.d_local, args.d_global, device=device) * 0.02,
|
|
67
|
+
"W_v_cross": torch.randn(args.d_local, args.d_global, device=device) * 0.02,
|
|
68
|
+
"W_o_cross": torch.randn(args.d_local, args.d_local, device=device) * 0.02,
|
|
69
|
+
"self_norm_w": torch.ones(args.d_local, device=device),
|
|
70
|
+
"W_q_self": torch.randn(args.d_local, args.d_local, device=device) * 0.02,
|
|
71
|
+
"W_k_self": torch.randn(args.d_local, args.d_local, device=device) * 0.02,
|
|
72
|
+
"W_v_self": torch.randn(args.d_local, args.d_local, device=device) * 0.02,
|
|
73
|
+
"W_o_self": torch.randn(args.d_local, args.d_local, device=device) * 0.02,
|
|
74
|
+
"dec_mlp_norm": torch.ones(args.d_local, device=device),
|
|
75
|
+
"W_gate_dec": torch.randn(args.d_local * 3, args.d_local, device=device) * 0.02,
|
|
76
|
+
"W_up_dec": torch.randn(args.d_local * 3, args.d_local, device=device) * 0.02,
|
|
77
|
+
"W_down_dec": torch.randn(args.d_local, args.d_local * 3, device=device) * 0.02,
|
|
78
|
+
"dec_final_norm":torch.ones(args.d_local, device=device),
|
|
79
|
+
"lm_head": torch.randn(256, args.d_local, device=device) * 0.02,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Populate 96 layers
|
|
83
|
+
for i in range(args.num_layers):
|
|
84
|
+
params[f"layer_{i}"] = {
|
|
85
|
+
"norm1_w": torch.ones(args.d_global, device=device),
|
|
86
|
+
"W_q": torch.randn(args.d_global, args.d_global, device=device) * 0.02,
|
|
87
|
+
"W_k": torch.randn(args.d_global, args.d_global, device=device) * 0.02,
|
|
88
|
+
"W_v": torch.randn(args.d_global, args.d_global, device=device) * 0.02,
|
|
89
|
+
"W_o": torch.randn(args.d_global, args.d_global, device=device) * 0.02,
|
|
90
|
+
"q_norm_w": torch.ones(head_dim, device=device),
|
|
91
|
+
"k_norm_w": torch.ones(head_dim, device=device),
|
|
92
|
+
"norm2_w": torch.ones(args.d_global, device=device),
|
|
93
|
+
"W_gate": torch.randn(d_mlp, args.d_global, device=device) * 0.02,
|
|
94
|
+
"W_up": torch.randn(d_mlp, args.d_global, device=device) * 0.02,
|
|
95
|
+
"W_down": torch.randn(args.d_global, d_mlp, device=device) * 0.02,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Enable autograd for all weights
|
|
99
|
+
for v in params.values():
|
|
100
|
+
if isinstance(v, dict):
|
|
101
|
+
for sub_v in v.values():
|
|
102
|
+
sub_v.requires_grad_(True)
|
|
103
|
+
else:
|
|
104
|
+
v.requires_grad_(True)
|
|
105
|
+
|
|
106
|
+
return params
|
|
107
|
+
|
|
108
|
+
def main():
|
|
109
|
+
args = parse_args()
|
|
110
|
+
device = torch.device(args.device)
|
|
111
|
+
print(f">> Initializing BLT 96-layer trainer on {device} (Compile={args.compile})...")
|
|
112
|
+
|
|
113
|
+
# 1. Initialize Model Weights
|
|
114
|
+
params = init_all_parameters(args, device)
|
|
115
|
+
head_dim = args.d_global // args.num_heads
|
|
116
|
+
cos, sin = precompute_rope_freqs(head_dim, args.max_patches, device=device)
|
|
117
|
+
|
|
118
|
+
# 2. Partition into Muon and AdamW Parameter Sets
|
|
119
|
+
muon_tensors, adam_tensors = partition_params_for_optimizers(params)
|
|
120
|
+
opt_muon = MuonWithScheduleSkip(muon_tensors, lr=args.lr_muon)
|
|
121
|
+
opt_adam = ScheduleFreeAdamW(adam_tensors, lr=args.lr_adam)
|
|
122
|
+
|
|
123
|
+
# 3. Build Dataloader from streaming weighted mix
|
|
124
|
+
print(">> Initializing Hugging Face multi-stream datamix (DCLM, FLAN, WikiMCQA, Reasoning, Ubuntu)...")
|
|
125
|
+
loader = get_mix_loader(block_size=args.block_size, batch_size=args.batch_size)
|
|
126
|
+
data_iter = iter(loader)
|
|
127
|
+
|
|
128
|
+
# 4. Training Loop
|
|
129
|
+
print(f">> Training starting: {args.steps} steps.")
|
|
130
|
+
for step in range(1, args.steps + 1):
|
|
131
|
+
batch_x, batch_y = next(data_iter)
|
|
132
|
+
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
|
|
133
|
+
|
|
134
|
+
opt_muon.zero_grad()
|
|
135
|
+
opt_adam.zero_grad()
|
|
136
|
+
|
|
137
|
+
# Step 1: Dynamic entropy-based patch extraction
|
|
138
|
+
# (Uniform patch fallback indexing for the decoder byte map)
|
|
139
|
+
chunk = args.block_size // args.max_patches
|
|
140
|
+
byte_to_patch = (torch.arange(args.block_size, device=device) // chunk).unsqueeze(0).expand(args.batch_size, -1)
|
|
141
|
+
|
|
142
|
+
# Batch forward pass via functional calls
|
|
143
|
+
# 1. Local Encoder
|
|
144
|
+
patches = torch.vmap(
|
|
145
|
+
blt_local_encoder_with_surprise,
|
|
146
|
+
in_dims=(None, 0, None, None)
|
|
147
|
+
)(params, batch_x, args.entropy_tau, args.max_patches)
|
|
148
|
+
|
|
149
|
+
# 2. 96-Layer Latent Backbone
|
|
150
|
+
backbone_out = torch.vmap(
|
|
151
|
+
global_96_layer_backbone_single,
|
|
152
|
+
in_dims=(0, None, None, None, None, None, None)
|
|
153
|
+
)(patches, params, cos, sin, args.num_heads, head_dim, args.num_layers)
|
|
154
|
+
|
|
155
|
+
# 3. Local Decoder Logits
|
|
156
|
+
h_decoded = torch.vmap(
|
|
157
|
+
local_decoder_single,
|
|
158
|
+
in_dims=(0, 0, 0, None, None, None)
|
|
159
|
+
)(batch_x, backbone_out, byte_to_patch, params, args.d_local, 4)
|
|
160
|
+
|
|
161
|
+
# 4. Chunked Loss calculation (Cut Cross-Entropy)
|
|
162
|
+
loss = cut_cross_entropy(h_decoded, params["lm_head"], batch_y, chunk_size=512)
|
|
163
|
+
loss.backward()
|
|
164
|
+
|
|
165
|
+
opt_muon.step()
|
|
166
|
+
opt_adam.step()
|
|
167
|
+
|
|
168
|
+
if step % 10 == 0 or step == 1:
|
|
169
|
+
print(f"Step {step:05d} | Loss: {loss.item():.4f} | Processed: {step * args.batch_size * args.block_size} bytes")
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
from torch.utils.data import IterableDataset, DataLoader
|
|
5
|
+
from datasets import load_dataset
|
|
6
|
+
|
|
7
|
+
# Normalized mixture weights based on the ablation spec
|
|
8
|
+
MIXTURE_WEIGHTS = {
|
|
9
|
+
"dclm": 800.0 / 1175.0,
|
|
10
|
+
"flan": 200.0 / 1175.0,
|
|
11
|
+
"wiki_mcqa": 100.0 / 1175.0,
|
|
12
|
+
"reasoning": 50.0 / 1175.0,
|
|
13
|
+
"ubuntu": 25.0 / 1175.0,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def create_stream_iterators():
|
|
17
|
+
"""Initializes streaming iterables from Hugging Face for each split."""
|
|
18
|
+
# 1. DCLM Baseline
|
|
19
|
+
ds_dclm = iter(load_dataset("mlfoundations/dclm-baseline-1.0", split="train", streaming=True))
|
|
20
|
+
|
|
21
|
+
# 2. FLAN (Collection / CoT)
|
|
22
|
+
ds_flan = iter(load_dataset("Muennighoff/flan", split="train", streaming=True))
|
|
23
|
+
|
|
24
|
+
# 3. Wikipedia MCQA (e.g. WikiQA or OpenBookQA/Wikipedia)
|
|
25
|
+
ds_wiki = iter(load_dataset("wiki_qa", split="train", streaming=True))
|
|
26
|
+
|
|
27
|
+
# 4. Reasoning Traces (OpenMath / GSM8k / NumGLUE style reasoning)
|
|
28
|
+
ds_reasoning = iter(load_dataset("open-web-math/open-web-math", split="train", streaming=True))
|
|
29
|
+
|
|
30
|
+
# 5. Ubuntu IRC Dialogue
|
|
31
|
+
ds_ubuntu = iter(load_dataset("sedthh/ubuntu_dialogue_qa", split="train", streaming=True))
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"dclm": (ds_dclm, lambda x: x.get("text", "")),
|
|
35
|
+
"flan": (ds_flan, lambda x: f"Question: {x.get('inputs', '')}\nAnswer: {x.get('targets', '')}\n"),
|
|
36
|
+
"wiki_mcqa": (ds_wiki, lambda x: f"Question: {x.get('question', '')}\nContext: {x.get('document_title', '')}\nAnswer: {x.get('answer', '')}\n"),
|
|
37
|
+
"reasoning": (ds_reasoning, lambda x: x.get("text", "")),
|
|
38
|
+
"ubuntu": (ds_ubuntu, lambda x: f"User: {x.get('question', '')}\nReply: {x.get('answer', '')}\n"),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class WeightedByteMixDataset(IterableDataset):
|
|
42
|
+
def __init__(self, block_size: int = 512, seed: int = 42):
|
|
43
|
+
self.block_size = block_size
|
|
44
|
+
self.seed = seed
|
|
45
|
+
|
|
46
|
+
def __iter__(self):
|
|
47
|
+
random.seed(self.seed)
|
|
48
|
+
streams = create_stream_iterators()
|
|
49
|
+
sources = list(MIXTURE_WEIGHTS.keys())
|
|
50
|
+
weights = [MIXTURE_WEIGHTS[s] for s in sources]
|
|
51
|
+
|
|
52
|
+
byte_buffer = bytearray()
|
|
53
|
+
needed = self.block_size + 1
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
# Sample which dataset to fetch from based on mix ratio
|
|
57
|
+
chosen_source = random.choices(sources, weights=weights, k=1)[0]
|
|
58
|
+
stream, text_extractor = streams[chosen_source]
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
sample = next(stream)
|
|
62
|
+
text = text_extractor(sample) + "\n"
|
|
63
|
+
except StopIteration:
|
|
64
|
+
# Re-seed exhaustion fallback
|
|
65
|
+
streams = create_stream_iterators()
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
byte_buffer.extend(text.encode("utf-8", errors="ignore"))
|
|
69
|
+
|
|
70
|
+
# Drain full blocks from buffer
|
|
71
|
+
while len(byte_buffer) >= needed:
|
|
72
|
+
chunk = bytes(byte_buffer[:needed])
|
|
73
|
+
del byte_buffer[:self.block_size]
|
|
74
|
+
|
|
75
|
+
arr = np.frombuffer(chunk, dtype=np.uint8).astype(np.int64)
|
|
76
|
+
x = torch.from_numpy(arr[:-1])
|
|
77
|
+
y = torch.from_numpy(arr[1:])
|
|
78
|
+
yield x, y
|
|
79
|
+
|
|
80
|
+
def get_mix_loader(block_size: int, batch_size: int, num_workers: int = 2):
|
|
81
|
+
dataset = WeightedByteMixDataset(block_size=block_size)
|
|
82
|
+
return DataLoader(
|
|
83
|
+
dataset,
|
|
84
|
+
batch_size=batch_size,
|
|
85
|
+
num_workers=num_workers,
|
|
86
|
+
pin_memory=True,
|
|
87
|
+
drop_last=True
|
|
88
|
+
)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn.functional as F
|
|
3
|
+
|
|
4
|
+
# =====================================================================
|
|
5
|
+
# 1. Positional Embeddings & Normalization Utilities
|
|
6
|
+
# =====================================================================
|
|
7
|
+
|
|
8
|
+
def rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
|
|
9
|
+
"""Root Mean Square Layer Normalization[cite: 1]."""
|
|
10
|
+
var = x.pow(2).mean(-1, keepdim=True)
|
|
11
|
+
return x * torch.rsqrt(var + eps) * weight
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def precompute_rope_freqs(
|
|
15
|
+
head_dim: int, max_seq_len: int, theta: float = 10000.0, device: str = "cpu"
|
|
16
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
17
|
+
"""Precomputes complex exponential cos/sin tables for RoPE[cite: 1]."""
|
|
18
|
+
freqs = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
|
|
19
|
+
t = torch.arange(max_seq_len, device=device).float()
|
|
20
|
+
freqs = torch.outer(t, freqs)
|
|
21
|
+
cos = torch.cos(freqs).repeat_interleave(2, dim=-1).unsqueeze(0)
|
|
22
|
+
sin = torch.sin(freqs).repeat_interleave(2, dim=-1).unsqueeze(0)
|
|
23
|
+
return cos, sin
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
|
27
|
+
"""Applies Rotary Position Embeddings to Q or K tensors[cite: 1]."""
|
|
28
|
+
# x: [num_heads, seq_len, head_dim]
|
|
29
|
+
half = x.shape[-1] // 2
|
|
30
|
+
x1, x2 = x[..., :half], x[..., half:]
|
|
31
|
+
rotated = torch.cat((-x2, x1), dim=-1)
|
|
32
|
+
return (x * cos) + (rotated * sin)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =====================================================================
|
|
36
|
+
# 2. Local Byte Encoder with Dynamic Surprise Factor
|
|
37
|
+
# =====================================================================
|
|
38
|
+
|
|
39
|
+
def compute_entropy_and_patches(
|
|
40
|
+
byte_ids: torch.Tensor, # [L]
|
|
41
|
+
byte_embed: torch.Tensor, # [256, d_small]
|
|
42
|
+
W_lm: torch.Tensor, # [256, d_small]
|
|
43
|
+
entropy_threshold: float,
|
|
44
|
+
max_patches: int,
|
|
45
|
+
max_patch_len: int = 16,
|
|
46
|
+
) -> torch.Tensor:
|
|
47
|
+
"""Calculates byte-level cross-entropy surprise and returns a boolean patch mask[cite: 1]."""
|
|
48
|
+
L = byte_ids.shape[0]
|
|
49
|
+
|
|
50
|
+
# 1. Predict next byte logits with a shallow byte model[cite: 1]
|
|
51
|
+
x_small = F.embedding(byte_ids, byte_embed) # [L, d_small][cite: 1]
|
|
52
|
+
logits = F.linear(x_small[:-1], W_lm) # [L - 1, 256][cite: 1]
|
|
53
|
+
targets = byte_ids[1:] # [L - 1][cite: 1]
|
|
54
|
+
|
|
55
|
+
# 2. Surprise factor: negative log likelihood[cite: 1]
|
|
56
|
+
log_probs = F.log_softmax(logits, dim=-1) #[cite: 1]
|
|
57
|
+
surprise = -log_probs.gather(dim=-1, index=targets.unsqueeze(-1)).squeeze(-1) #[cite: 1]
|
|
58
|
+
surprise = torch.cat([torch.tensor([0.0], device=byte_ids.device), surprise]) # [L][cite: 1]
|
|
59
|
+
|
|
60
|
+
# 3. Mark boundaries where surprise spikes[cite: 1]
|
|
61
|
+
is_spike = surprise >= entropy_threshold #[cite: 1]
|
|
62
|
+
patch_mask = torch.zeros((max_patches, L), dtype=torch.bool, device=byte_ids.device) #[cite: 1]
|
|
63
|
+
|
|
64
|
+
curr_patch = 0 #[cite: 1]
|
|
65
|
+
patch_start = 0 #[cite: 1]
|
|
66
|
+
for t in range(L):
|
|
67
|
+
patch_mask[curr_patch, t] = True #[cite: 1]
|
|
68
|
+
reach_max = (t - patch_start + 1) >= max_patch_len #[cite: 1]
|
|
69
|
+
boundary = (is_spike[t] and (t > patch_start)) or reach_max #[cite: 1]
|
|
70
|
+
|
|
71
|
+
if boundary and (curr_patch < max_patches - 1): #[cite: 1]
|
|
72
|
+
curr_patch += 1 #[cite: 1]
|
|
73
|
+
patch_start = t + 1 #[cite: 1]
|
|
74
|
+
|
|
75
|
+
return patch_mask #[cite: 1]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def blt_local_encoder_with_surprise(
|
|
79
|
+
params: dict,
|
|
80
|
+
byte_ids: torch.Tensor, # [L]
|
|
81
|
+
entropy_threshold: float,
|
|
82
|
+
max_patches: int,
|
|
83
|
+
) -> torch.Tensor:
|
|
84
|
+
"""Maps raw byte tokens to [max_patches, d_global] using cross-attention pooling[cite: 1]."""
|
|
85
|
+
d_global = params["W_out_enc"].shape[0]
|
|
86
|
+
scale = d_global ** -0.5 #[cite: 1]
|
|
87
|
+
|
|
88
|
+
# Dynamically identify boundaries[cite: 1]
|
|
89
|
+
patch_mask = compute_entropy_and_patches(
|
|
90
|
+
byte_ids=byte_ids,
|
|
91
|
+
byte_embed=params["entropy_embed"],
|
|
92
|
+
W_lm=params["entropy_head"],
|
|
93
|
+
entropy_threshold=entropy_threshold,
|
|
94
|
+
max_patches=max_patches,
|
|
95
|
+
) #[cite: 1]
|
|
96
|
+
|
|
97
|
+
# Intra-patch contextualization (1D convolution)[cite: 1]
|
|
98
|
+
x = F.embedding(byte_ids, params["byte_embed"]) #[cite: 1]
|
|
99
|
+
x_conv = x.transpose(0, 1).unsqueeze(0) #[cite: 1]
|
|
100
|
+
pad = (params["conv_w"].shape[-1] - 1) // 2 #[cite: 1]
|
|
101
|
+
x_conv = F.conv1d(x_conv, params["conv_w"], padding=pad, groups=x.shape[-1]) #[cite: 1]
|
|
102
|
+
x_ctx = F.silu(x_conv).squeeze(0).transpose(0, 1) # [L, d_local][cite: 1]
|
|
103
|
+
|
|
104
|
+
# Projections to global space[cite: 1]
|
|
105
|
+
k = F.linear(x_ctx, params["W_k_enc"]) #[cite: 1]
|
|
106
|
+
v = F.linear(x_ctx, params["W_v_enc"]) #[cite: 1]
|
|
107
|
+
q = params["q_seed"] # [max_patches, d_global][cite: 1]
|
|
108
|
+
|
|
109
|
+
# Cross-attention patch pooling[cite: 1]
|
|
110
|
+
scores = torch.matmul(q, k.transpose(0, 1)) * scale #[cite: 1]
|
|
111
|
+
empty_patches = ~patch_mask.any(dim=-1, keepdim=True) #[cite: 1]
|
|
112
|
+
mask_for_scores = patch_mask | empty_patches #[cite: 1]
|
|
113
|
+
scores = scores.masked_fill(~mask_for_scores, float("-inf")) #[cite: 1]
|
|
114
|
+
|
|
115
|
+
attn_weights = F.softmax(scores, dim=-1) #[cite: 1]
|
|
116
|
+
pooled = torch.matmul(attn_weights, v) #[cite: 1]
|
|
117
|
+
pooled = torch.where(empty_patches, torch.zeros_like(pooled), pooled) #[cite: 1]
|
|
118
|
+
|
|
119
|
+
return F.linear(pooled, params["W_out_enc"])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# =====================================================================
|
|
123
|
+
# 3. 96-Layer Global Backbone (MHA + QK-Norm + SwiGLU)
|
|
124
|
+
# =====================================================================
|
|
125
|
+
|
|
126
|
+
def narrow_deep_block(
|
|
127
|
+
x: torch.Tensor, # [P, d][cite: 1]
|
|
128
|
+
p: dict,
|
|
129
|
+
cos: torch.Tensor,
|
|
130
|
+
sin: torch.Tensor,
|
|
131
|
+
num_heads: int,
|
|
132
|
+
head_dim: int,
|
|
133
|
+
res_scale: float,
|
|
134
|
+
) -> torch.Tensor:
|
|
135
|
+
"""Single transformer layer with depth-scaled residuals and QK-norm[cite: 1]."""
|
|
136
|
+
P, d = x.shape #[cite: 1]
|
|
137
|
+
scale = head_dim ** -0.5 #[cite: 1]
|
|
138
|
+
|
|
139
|
+
# --- Pre-LN MHA with QK-Norm ---[cite: 1]
|
|
140
|
+
h = rms_norm(x, p["norm1_w"]) #[cite: 1]
|
|
141
|
+
|
|
142
|
+
q = F.linear(h, p["W_q"]).view(P, num_heads, head_dim).transpose(0, 1) # [H, P, d_k][cite: 1]
|
|
143
|
+
k = F.linear(h, p["W_k"]).view(P, num_heads, head_dim).transpose(0, 1) # [H, P, d_k][cite: 1]
|
|
144
|
+
v = F.linear(h, p["W_v"]).view(P, num_heads, head_dim).transpose(0, 1) # [H, P, d_k][cite: 1]
|
|
145
|
+
|
|
146
|
+
# Normalize queries and keys to prevent logit explosion across deep layers[cite: 1]
|
|
147
|
+
q = rms_norm(q, p["q_norm_w"]) #[cite: 1]
|
|
148
|
+
k = rms_norm(k, p["k_norm_w"]) #[cite: 1]
|
|
149
|
+
|
|
150
|
+
q = apply_rope(q, cos, sin) #[cite: 1]
|
|
151
|
+
k = apply_rope(k, cos, sin) #[cite: 1]
|
|
152
|
+
|
|
153
|
+
# Causal self-attention[cite: 1]
|
|
154
|
+
scores = torch.bmm(q, k.transpose(1, 2)) * scale #[cite: 1]
|
|
155
|
+
causal_mask = torch.triu(torch.full((P, P), float("-inf"), device=x.device), diagonal=1) #[cite: 1]
|
|
156
|
+
scores = scores + causal_mask.unsqueeze(0) #[cite: 1]
|
|
157
|
+
attn_weights = F.softmax(scores, dim=-1) #[cite: 1]
|
|
158
|
+
|
|
159
|
+
attn_out = torch.bmm(attn_weights, v).transpose(0, 1).contiguous().view(P, num_heads * head_dim) #[cite: 1]
|
|
160
|
+
h_attn = F.linear(attn_out, p["W_o"]) #[cite: 1]
|
|
161
|
+
x = x + (res_scale * h_attn) #[cite: 1]
|
|
162
|
+
|
|
163
|
+
# --- Pre-LN SwiGLU MLP ---[cite: 1]
|
|
164
|
+
h_mlp = rms_norm(x, p["norm2_w"]) #[cite: 1]
|
|
165
|
+
gate = F.linear(h_mlp, p["W_gate"]) #[cite: 1]
|
|
166
|
+
up = F.linear(h_mlp, p["W_up"]) #[cite: 1]
|
|
167
|
+
swiglu = F.silu(gate) * up #[cite: 1]
|
|
168
|
+
x_mlp = F.linear(swiglu, p["W_down"]) #[cite: 1]
|
|
169
|
+
|
|
170
|
+
return x + (res_scale * x_mlp) #[cite: 1]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def global_96_layer_backbone_single(
|
|
174
|
+
patches: torch.Tensor, # [P, d][cite: 1]
|
|
175
|
+
params: dict,
|
|
176
|
+
cos: torch.Tensor,
|
|
177
|
+
sin: torch.Tensor,
|
|
178
|
+
num_heads: int = 6,
|
|
179
|
+
head_dim: int = 48,
|
|
180
|
+
num_layers: int = 96,
|
|
181
|
+
) -> torch.Tensor:
|
|
182
|
+
"""Iterates through all 96 blocks with scaled residual updates[cite: 1]."""
|
|
183
|
+
h = patches #[cite: 1]
|
|
184
|
+
res_scale = (2.0 * num_layers) ** -0.5 #[cite: 1]
|
|
185
|
+
|
|
186
|
+
for i in range(num_layers): #[cite: 1]
|
|
187
|
+
block_p = params[f"layer_{i}"] #[cite: 1]
|
|
188
|
+
h = narrow_deep_block( #[cite: 1]
|
|
189
|
+
h, block_p, cos, sin,
|
|
190
|
+
num_heads=num_heads,
|
|
191
|
+
head_dim=head_dim,
|
|
192
|
+
res_scale=res_scale,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return rms_norm(h, params["final_norm_w"]) #[cite: 1]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# =====================================================================
|
|
199
|
+
# 4. Local Byte Decoder
|
|
200
|
+
# =====================================================================
|
|
201
|
+
|
|
202
|
+
def local_decoder_single(
|
|
203
|
+
byte_tokens: torch.Tensor, # [L][cite: 1]
|
|
204
|
+
latent_patches: torch.Tensor, # [P, d_global][cite: 1]
|
|
205
|
+
byte_to_patch_idx: torch.Tensor, # [L][cite: 1]
|
|
206
|
+
params: dict,
|
|
207
|
+
d_local: int = 128,
|
|
208
|
+
num_heads: int = 4,
|
|
209
|
+
) -> torch.Tensor:
|
|
210
|
+
"""Cross-attends from bytes to global patches and runs local byte self-attention[cite: 1]."""
|
|
211
|
+
L = byte_tokens.shape[0] #[cite: 1]
|
|
212
|
+
P, d_global = latent_patches.shape #[cite: 1]
|
|
213
|
+
head_dim = d_local // num_heads #[cite: 1]
|
|
214
|
+
scale = head_dim ** -0.5 #[cite: 1]
|
|
215
|
+
|
|
216
|
+
# Byte Embedding Lookup[cite: 1]
|
|
217
|
+
x_bytes = F.embedding(byte_tokens, params["byte_embed"]) #[cite: 1]
|
|
218
|
+
|
|
219
|
+
# --- Patch Cross-Attention ---[cite: 1]
|
|
220
|
+
h_bytes = rms_norm(x_bytes, params["cross_norm_w"]) #[cite: 1]
|
|
221
|
+
q_b = F.linear(h_bytes, params["W_q_cross"]).view(L, num_heads, head_dim).transpose(0, 1) #[cite: 1]
|
|
222
|
+
k_p = F.linear(latent_patches, params["W_k_cross"]).view(P, num_heads, head_dim).transpose(0, 1) #[cite: 1]
|
|
223
|
+
v_p = F.linear(latent_patches, params["W_v_cross"]).view(P, num_heads, head_dim).transpose(0, 1) #[cite: 1]
|
|
224
|
+
|
|
225
|
+
cross_scores = torch.bmm(q_b, k_p.transpose(1, 2)) * scale #[cite: 1]
|
|
226
|
+
|
|
227
|
+
# Byte t cannot attend to future patches (p' > p)[cite: 1]
|
|
228
|
+
p_indices = torch.arange(P, device=byte_tokens.device).unsqueeze(0) #[cite: 1]
|
|
229
|
+
b_indices = byte_to_patch_idx.unsqueeze(1) #[cite: 1]
|
|
230
|
+
causal_patch_mask = (p_indices > b_indices).unsqueeze(0) #[cite: 1]
|
|
231
|
+
cross_scores = cross_scores.masked_fill(causal_patch_mask, float("-inf")) #[cite: 1]
|
|
232
|
+
cross_weights = F.softmax(cross_scores, dim=-1) #[cite: 1]
|
|
233
|
+
|
|
234
|
+
cross_out = torch.bmm(cross_weights, v_p).transpose(0, 1).contiguous().view(L, d_local) #[cite: 1]
|
|
235
|
+
x_bytes = x_bytes + F.linear(cross_out, params["W_o_cross"]) #[cite: 1]
|
|
236
|
+
|
|
237
|
+
# --- Local Intra-Patch Causal Self-Attention ---[cite: 1]
|
|
238
|
+
h_self = rms_norm(x_bytes, params["self_norm_w"]) #[cite: 1]
|
|
239
|
+
q_s = F.linear(h_self, params["W_q_self"]).view(L, num_heads, head_dim).transpose(0, 1) #[cite: 1]
|
|
240
|
+
k_s = F.linear(h_self, params["W_k_self"]).view(L, num_heads, head_dim).transpose(0, 1) #[cite: 1]
|
|
241
|
+
v_s = F.linear(h_self, params["W_v_self"]).view(L, num_heads, head_dim).transpose(0, 1) #[cite: 1]
|
|
242
|
+
|
|
243
|
+
self_scores = torch.bmm(q_s, k_s.transpose(1, 2)) * scale #[cite: 1]
|
|
244
|
+
causal_byte_mask = torch.triu(torch.full((L, L), float("-inf"), device=byte_tokens.device), diagonal=1) #[cite: 1]
|
|
245
|
+
self_scores = self_scores + causal_byte_mask.unsqueeze(0) #[cite: 1]
|
|
246
|
+
self_weights = F.softmax(self_scores, dim=-1) #[cite: 1]
|
|
247
|
+
|
|
248
|
+
self_out = torch.bmm(self_weights, v_s).transpose(0, 1).contiguous().view(L, d_local) #[cite: 1]
|
|
249
|
+
x_bytes = x_bytes + F.linear(self_out, params["W_o_self"]) #[cite: 1]
|
|
250
|
+
|
|
251
|
+
# --- Local SwiGLU MLP ---
|
|
252
|
+
h_mlp = rms_norm(x_bytes, params["dec_mlp_norm"])
|
|
253
|
+
gate = F.linear(h_mlp, params["W_gate_dec"])
|
|
254
|
+
up = F.linear(h_mlp, params["W_up_dec"])
|
|
255
|
+
x_bytes = x_bytes + F.linear(F.silu(gate) * up, params["W_down_dec"])
|
|
256
|
+
|
|
257
|
+
# --- Final Output Projection ---
|
|
258
|
+
return rms_norm(x_bytes, params["dec_final_norm"])
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn.functional as F
|
|
3
|
+
from torch.optim.optimizer import Optimizer
|
|
4
|
+
|
|
5
|
+
# =====================================================================
|
|
6
|
+
# 1. Cut Cross-Entropy (Chunked Loss)
|
|
7
|
+
# =====================================================================
|
|
8
|
+
|
|
9
|
+
def cut_cross_entropy(
|
|
10
|
+
h_final: torch.Tensor, # [B, L, d_local]
|
|
11
|
+
lm_head: torch.Tensor, # [256, d_local]
|
|
12
|
+
targets: torch.Tensor, # [B, L]
|
|
13
|
+
chunk_size: int = 1024,
|
|
14
|
+
) -> torch.Tensor:
|
|
15
|
+
"""
|
|
16
|
+
Computes cross-entropy loss along the sequence dimension in slices,
|
|
17
|
+
preventing high memory consumption from materializing the entire logit tensor.
|
|
18
|
+
"""
|
|
19
|
+
B, L, D = h_final.shape
|
|
20
|
+
h_flat = h_final.view(-1, D)
|
|
21
|
+
targets_flat = targets.view(-1)
|
|
22
|
+
|
|
23
|
+
total_tokens = h_flat.size(0)
|
|
24
|
+
total_loss = torch.tensor(0.0, device=h_final.device)
|
|
25
|
+
|
|
26
|
+
for start in range(0, total_tokens, chunk_size):
|
|
27
|
+
end = min(start + chunk_size, total_tokens)
|
|
28
|
+
h_chunk = h_flat[start:end]
|
|
29
|
+
t_chunk = targets_flat[start:end]
|
|
30
|
+
|
|
31
|
+
logits_chunk = F.linear(h_chunk, lm_head) # [chunk, 256]
|
|
32
|
+
loss_chunk = F.cross_entropy(logits_chunk, t_chunk, reduction="sum")
|
|
33
|
+
total_loss = total_loss + loss_chunk
|
|
34
|
+
|
|
35
|
+
return total_loss / total_tokens
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# =====================================================================
|
|
39
|
+
# 2. Muon Optimizer with ScheduleSkip (Newton-Schulz Orthogonalization)
|
|
40
|
+
# =====================================================================
|
|
41
|
+
|
|
42
|
+
def zeropower_via_newtonschulz5(G: torch.Tensor, steps: int = 5, eps: float = 1e-7) -> torch.Tensor:
|
|
43
|
+
"""
|
|
44
|
+
Newton-Schulz iteration (degree 5) to compute the matrix sign/orthogonalization
|
|
45
|
+
of gradient tensors for Muon updates.
|
|
46
|
+
"""
|
|
47
|
+
assert G.ndim >= 2
|
|
48
|
+
a, b, c = (3.4445, -4.7750, 2.0315)
|
|
49
|
+
X = G.bfloat16() if G.is_cuda else G.float()
|
|
50
|
+
X /= (X.norm() + eps)
|
|
51
|
+
|
|
52
|
+
transposed = False
|
|
53
|
+
if X.size(0) > X.size(1):
|
|
54
|
+
X = X.T
|
|
55
|
+
transposed = True
|
|
56
|
+
|
|
57
|
+
for _ in range(steps):
|
|
58
|
+
A = X @ X.T
|
|
59
|
+
B = b * A + c * (A @ A)
|
|
60
|
+
X = a * X + B @ X
|
|
61
|
+
|
|
62
|
+
if transposed:
|
|
63
|
+
X = X.T
|
|
64
|
+
return X.type_as(G)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MuonWithScheduleSkip(Optimizer):
|
|
68
|
+
"""
|
|
69
|
+
Muon optimizer applied to hidden 2D projection matrices.
|
|
70
|
+
Implements schedule-free state tracking ('z') so learning rates do not
|
|
71
|
+
require fixed decay step counts.
|
|
72
|
+
"""
|
|
73
|
+
def __init__(self, params, lr: float = 0.02, momentum: float = 0.95, weight_decay: float = 0.01):
|
|
74
|
+
defaults = dict(lr=lr, momentum=momentum, weight_decay=weight_decay)
|
|
75
|
+
super().__init__(params, defaults)
|
|
76
|
+
|
|
77
|
+
@torch.no_grad()
|
|
78
|
+
def step(self):
|
|
79
|
+
for group in self.param_groups:
|
|
80
|
+
lr = group["lr"]
|
|
81
|
+
mom = group["momentum"]
|
|
82
|
+
wd = group["weight_decay"]
|
|
83
|
+
|
|
84
|
+
for p in group["params"]:
|
|
85
|
+
if p.grad is None:
|
|
86
|
+
continue
|
|
87
|
+
g = p.grad
|
|
88
|
+
state = self.state[p]
|
|
89
|
+
|
|
90
|
+
if "momentum_buffer" not in state:
|
|
91
|
+
state["momentum_buffer"] = torch.zeros_like(g)
|
|
92
|
+
state["z"] = p.clone()
|
|
93
|
+
|
|
94
|
+
buf = state["momentum_buffer"]
|
|
95
|
+
buf.mul_(mom).add_(g)
|
|
96
|
+
|
|
97
|
+
# Matrix orthogonalization
|
|
98
|
+
u = zeropower_via_newtonschulz5(buf)
|
|
99
|
+
|
|
100
|
+
# Weight decay applied directly to auxiliary sequence
|
|
101
|
+
state["z"].mul_(1.0 - lr * wd)
|
|
102
|
+
state["z"].add_(u, alpha=-lr)
|
|
103
|
+
|
|
104
|
+
p.copy_(state["z"])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# =====================================================================
|
|
108
|
+
# 3. Schedule-Free AdamW (for 1D Norms and Embeddings)
|
|
109
|
+
# =====================================================================
|
|
110
|
+
|
|
111
|
+
class ScheduleFreeAdamW(Optimizer):
|
|
112
|
+
"""
|
|
113
|
+
AdamW variant that maintains momentum buffers without requiring
|
|
114
|
+
a scheduled cosine learning rate multiplier.
|
|
115
|
+
"""
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
params,
|
|
119
|
+
lr: float = 1e-3,
|
|
120
|
+
betas: tuple[float, float] = (0.9, 0.999),
|
|
121
|
+
eps: float = 1e-8,
|
|
122
|
+
weight_decay: float = 0.0,
|
|
123
|
+
):
|
|
124
|
+
defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
|
|
125
|
+
super().__init__(params, defaults)
|
|
126
|
+
|
|
127
|
+
@torch.no_grad()
|
|
128
|
+
def step(self):
|
|
129
|
+
for group in self.param_groups:
|
|
130
|
+
lr = group["lr"]
|
|
131
|
+
beta1, beta2 = group["betas"]
|
|
132
|
+
eps = group["eps"]
|
|
133
|
+
wd = group["weight_decay"]
|
|
134
|
+
|
|
135
|
+
for p in group["params"]:
|
|
136
|
+
if p.grad is None:
|
|
137
|
+
continue
|
|
138
|
+
g = p.grad
|
|
139
|
+
state = self.state[p]
|
|
140
|
+
|
|
141
|
+
if "step" not in state:
|
|
142
|
+
state["step"] = 0
|
|
143
|
+
state["exp_avg"] = torch.zeros_like(g)
|
|
144
|
+
state["exp_avg_sq"] = torch.zeros_like(g)
|
|
145
|
+
state["z"] = p.clone()
|
|
146
|
+
|
|
147
|
+
state["step"] += 1
|
|
148
|
+
exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"]
|
|
149
|
+
|
|
150
|
+
exp_avg.mul_(beta1).add_(g, alpha=1.0 - beta1)
|
|
151
|
+
exp_avg_sq.mul_(beta2).addcmul_(g, g, value=1.0 - beta2)
|
|
152
|
+
|
|
153
|
+
denom = exp_avg_sq.sqrt().add_(eps)
|
|
154
|
+
step_size = lr / (1.0 - beta1 ** state["step"])
|
|
155
|
+
|
|
156
|
+
state["z"].mul_(1.0 - lr * wd)
|
|
157
|
+
state["z"].addcdiv_(exp_avg, denom, value=-step_size)
|
|
158
|
+
|
|
159
|
+
p.copy_(state["z"])
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# =====================================================================
|
|
163
|
+
# 4. Optimizer Parameter Routing
|
|
164
|
+
# =====================================================================
|
|
165
|
+
|
|
166
|
+
def partition_params_for_optimizers(params: dict) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
|
|
167
|
+
"""
|
|
168
|
+
Traverses nested parameter dictionaries and separates parameters:
|
|
169
|
+
- 2D internal weight matrices -> Muon
|
|
170
|
+
- 1D normalization gains, biases, and embedding tables -> AdamW
|
|
171
|
+
"""
|
|
172
|
+
muon_tensors = []
|
|
173
|
+
adam_tensors = []
|
|
174
|
+
|
|
175
|
+
def _collect(subdict, prefix=""):
|
|
176
|
+
for k, v in subdict.items():
|
|
177
|
+
full_name = f"{prefix}.{k}" if prefix else k
|
|
178
|
+
if isinstance(v, dict):
|
|
179
|
+
_collect(v, full_name)
|
|
180
|
+
elif isinstance(v, torch.Tensor):
|
|
181
|
+
# Only 2D hidden matrices route to Muon
|
|
182
|
+
if v.ndim == 2 and not any(tag in full_name for tag in ["embed", "lm_head", "entropy"]):
|
|
183
|
+
muon_tensors.append(v)
|
|
184
|
+
else:
|
|
185
|
+
adam_tensors.append(v)
|
|
186
|
+
|
|
187
|
+
_collect(params)
|
|
188
|
+
return muon_tensors, adam_tensors
|