asterocr 1.0.0__py3-none-any.whl
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.
- asterocr/__init__.py +12 -0
- asterocr/cli.py +35 -0
- asterocr/comparator.py +412 -0
- asterocr/dataset/export_real_zip.py +117 -0
- asterocr/dataset/loader.py +371 -0
- asterocr/dataset/unified_packer.py +402 -0
- asterocr/download_dataset.py +93 -0
- asterocr/inference/decoder.py +302 -0
- asterocr/inference/lexicon.py +136 -0
- asterocr/inference/predictor.py +205 -0
- asterocr/inference/viet_linguistics.py +340 -0
- asterocr/kaggle_bridge/__init__.py +8 -0
- asterocr/kaggle_bridge/chunker.py +252 -0
- asterocr/kaggle_bridge/colab_auto_runner.py +380 -0
- asterocr/kaggle_bridge/kaggle_worker.py +1911 -0
- asterocr/kaggle_bridge/launcher.py +245 -0
- asterocr/kaggle_bridge/tunnel.py +190 -0
- asterocr/models/backbone.py +102 -0
- asterocr/models/language.py +104 -0
- asterocr/models/model.py +113 -0
- asterocr/models/sequence.py +34 -0
- asterocr/pipeline.py +179 -0
- asterocr/studio.py +371 -0
- asterocr/synthesizer/corpus.py +378 -0
- asterocr/synthesizer/font_manager.py +170 -0
- asterocr/synthesizer/generator.py +205 -0
- asterocr/synthesizer/handwriting_generator.py +420 -0
- asterocr/synthesizer/mega_generator.py +527 -0
- asterocr/trainer/metrics.py +53 -0
- asterocr/trainer/trainer.py +1295 -0
- asterocr/vocab.py +175 -0
- asterocr-1.0.0.dist-info/METADATA +102 -0
- asterocr-1.0.0.dist-info/RECORD +36 -0
- asterocr-1.0.0.dist-info/WHEEL +5 -0
- asterocr-1.0.0.dist-info/entry_points.txt +2 -0
- asterocr-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1911 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Kaggle Cloud Worker (Self-Contained)
|
|
3
|
+
Runs on NVIDIA Tesla P100 (16GB VRAM) in Kaggle Cloud.
|
|
4
|
+
Streams chunks from local machine via Cloudflare Tunnel, trains with FP16 Mixed Precision,
|
|
5
|
+
streams real-time telemetry back to local Web GUI, and syncs checkpoints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import io
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import json
|
|
13
|
+
import struct
|
|
14
|
+
import base64
|
|
15
|
+
import gc
|
|
16
|
+
import queue
|
|
17
|
+
import threading
|
|
18
|
+
import concurrent.futures
|
|
19
|
+
import requests
|
|
20
|
+
from PIL import Image
|
|
21
|
+
import numpy as np
|
|
22
|
+
import math
|
|
23
|
+
|
|
24
|
+
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
import torch.nn as nn
|
|
28
|
+
import torch.optim as optim
|
|
29
|
+
from torch.nn import functional as F
|
|
30
|
+
from torch.utils.data import Dataset, DataLoader
|
|
31
|
+
import numpy as np
|
|
32
|
+
import base64
|
|
33
|
+
from io import BytesIO
|
|
34
|
+
from PIL import Image
|
|
35
|
+
|
|
36
|
+
def tensor_to_b64(tensor):
|
|
37
|
+
try:
|
|
38
|
+
arr = (tensor.cpu().numpy().transpose(1, 2, 0) * 255).astype(np.uint8)
|
|
39
|
+
if arr.shape[2] == 1:
|
|
40
|
+
arr = np.squeeze(arr, axis=2)
|
|
41
|
+
img = Image.fromarray(arr)
|
|
42
|
+
buffered = BytesIO()
|
|
43
|
+
img.save(buffered, format="JPEG", quality=60)
|
|
44
|
+
return "data:image/jpeg;base64," + base64.b64encode(buffered.getvalue()).decode('utf-8')
|
|
45
|
+
except Exception:
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
import torch.distributed as dist
|
|
49
|
+
import torch.multiprocessing as mp
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =========================================================================
|
|
53
|
+
# 1. RUNTIME CONFIGURATION (Injected dynamically by launcher)
|
|
54
|
+
IS_TPU_DEVICE = False
|
|
55
|
+
TUNNEL_URL = "https://asterai.sglocalhub.com" # Live Cloudflare Named Tunnel endpoint (Permanent)
|
|
56
|
+
SESSION_TOKEN = "aster_token_default"
|
|
57
|
+
BATCH_SIZE = 96 # 96 samples per GPU -> 192 total across Dual Tesla T4 (High Tensor Core saturation + 100% OOM free with dynamic padding)
|
|
58
|
+
LEARNING_RATE = 0.00005 # Optimal Fine-Tuning LR (Protects pre-trained features while mastering hard samples)
|
|
59
|
+
TARGET_HEIGHT = 32
|
|
60
|
+
MAX_EPOCHS = 5
|
|
61
|
+
|
|
62
|
+
def compute_scheduled_lr(base_lr, cur_epoch, tot_epochs=5):
|
|
63
|
+
"""
|
|
64
|
+
Automatic Multi-Epoch Learning Rate Decay (Fine-Tuning Annealer):
|
|
65
|
+
- Epoch 1: 1.0x (0.000050 - Nắm bắt phân phối ảnh scan tiếng Việt mới)
|
|
66
|
+
- Epoch 2: 0.8x (0.000040 - Tinh chỉnh các đặc trưng nét chữ & hoa văn nền)
|
|
67
|
+
- Epoch 3: 0.5x (0.000025 - Ổn định và giảm nhiễu)
|
|
68
|
+
- Epoch 4: 0.3x (0.000015 - Hội tụ chi tiết số CCCD, tiền tệ)
|
|
69
|
+
- Epoch 5: 0.15x (0.000008 - Cực tiểu hóa toàn cục)
|
|
70
|
+
"""
|
|
71
|
+
factors = {
|
|
72
|
+
1: 1.0,
|
|
73
|
+
2: 0.8,
|
|
74
|
+
3: 0.5,
|
|
75
|
+
4: 0.3,
|
|
76
|
+
5: 0.15
|
|
77
|
+
}
|
|
78
|
+
ratio = factors.get(cur_epoch, 0.5 ** max(0, cur_epoch - 1))
|
|
79
|
+
return round(base_lr * ratio, 7)
|
|
80
|
+
|
|
81
|
+
# =========================================================================
|
|
82
|
+
# 2. VIETNAMESE VOCABULARY & ALPHABET
|
|
83
|
+
# =========================================================================
|
|
84
|
+
VIETNAMESE_LOWER = (
|
|
85
|
+
"aàáảãạăằắẳẵặâầấẩẫậ"
|
|
86
|
+
"bcdđeèéẻẽẹêềếểễệ"
|
|
87
|
+
"fghiìíỉĩịjklmnoòóỏõọôồốổỗộơờớởỡợ"
|
|
88
|
+
"pqrstuùúủũụưừứửữự"
|
|
89
|
+
"vwxyỳýỷỹỵz"
|
|
90
|
+
)
|
|
91
|
+
VIETNAMESE_UPPER = (
|
|
92
|
+
"AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬ"
|
|
93
|
+
"BCDĐEÈÉẺẼẸÊỀẾỂỄỆ"
|
|
94
|
+
"FGHIÌÍỈĨỊJKLMNOÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢ"
|
|
95
|
+
"PQRSTUÙÚỦŨỤƯỪỨỬỮỰ"
|
|
96
|
+
"VWXYỲÝỶỸỴZ"
|
|
97
|
+
)
|
|
98
|
+
DIGITS = "0123456789"
|
|
99
|
+
PUNCTUATION = " ,.-:;\"'?!()[]{}/%+$&=*#@_\\<>°"
|
|
100
|
+
CTC_BLANK = "<blank>"
|
|
101
|
+
VOCAB_CHARS = [CTC_BLANK] + list(dict.fromkeys(list(VIETNAMESE_LOWER + VIETNAMESE_UPPER + DIGITS + PUNCTUATION)))
|
|
102
|
+
CHAR2IDX = {c: i for i, c in enumerate(VOCAB_CHARS)}
|
|
103
|
+
BLANK_IDX = 0
|
|
104
|
+
|
|
105
|
+
UNICODE_NORM_MAP = {
|
|
106
|
+
'“': '"', '”': '"', '„': '"', '«': '"', '»': '"',
|
|
107
|
+
'’': "'", '‘': "'", '`': "'", '´': "'",
|
|
108
|
+
'–': '-', '—': '-', '−': '-', '‒': '-',
|
|
109
|
+
'…': '...', '•': '.',
|
|
110
|
+
'²': '2', '³': '3', '¹': '1',
|
|
111
|
+
'±': '+', '÷': '/', '×': '*',
|
|
112
|
+
'€': '$', '£': '$', '¥': '$',
|
|
113
|
+
'ā': 'a', 'Ā': 'A', 'ū': 'u', 'Ū': 'U', 'ō': 'o', 'Ō': 'O',
|
|
114
|
+
'ü': 'u', 'Ü': 'U', 'ð': 'd', 'Ð': 'D',
|
|
115
|
+
'|': '/'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def normalize_text(text):
|
|
119
|
+
if not text:
|
|
120
|
+
return ""
|
|
121
|
+
res = []
|
|
122
|
+
for c in text:
|
|
123
|
+
res.append(UNICODE_NORM_MAP.get(c, c))
|
|
124
|
+
return "".join(res)
|
|
125
|
+
|
|
126
|
+
def encode_text(text):
|
|
127
|
+
norm_text = normalize_text(text)
|
|
128
|
+
return [CHAR2IDX[c] for c in norm_text if c in CHAR2IDX]
|
|
129
|
+
|
|
130
|
+
def calculate_cer(reference, hypothesis):
|
|
131
|
+
import editdistance
|
|
132
|
+
total_dist = 0
|
|
133
|
+
total_len = 0
|
|
134
|
+
for r, h in zip(reference, hypothesis):
|
|
135
|
+
total_dist += editdistance.eval(r, h)
|
|
136
|
+
total_len += max(1, len(r))
|
|
137
|
+
return total_dist / max(1, total_len)
|
|
138
|
+
|
|
139
|
+
# =========================================================================
|
|
140
|
+
# 3. NEURAL NETWORK ARCHITECTURE: AsterNet (ResNet-SVTR + BiLSTM + CTC)
|
|
141
|
+
# =========================================================================
|
|
142
|
+
class SqueezeExcitation(nn.Module):
|
|
143
|
+
def __init__(self, channels, reduction=16):
|
|
144
|
+
super().__init__()
|
|
145
|
+
self.avg_pool = nn.AdaptiveAvgPool2d(1)
|
|
146
|
+
mid = max(8, channels // reduction)
|
|
147
|
+
self.fc = nn.Sequential(
|
|
148
|
+
nn.Linear(channels, mid, bias=False),
|
|
149
|
+
nn.SiLU(inplace=True),
|
|
150
|
+
nn.Linear(mid, channels, bias=False),
|
|
151
|
+
nn.Sigmoid()
|
|
152
|
+
)
|
|
153
|
+
def forward(self, x):
|
|
154
|
+
b, c, _, _ = x.size()
|
|
155
|
+
y = self.fc(self.avg_pool(x).view(b, c)).view(b, c, 1, 1)
|
|
156
|
+
return x * y.expand_as(x)
|
|
157
|
+
|
|
158
|
+
class HighFrequencyStem(nn.Module):
|
|
159
|
+
def __init__(self, in_channels=3, out_channels=64):
|
|
160
|
+
super().__init__()
|
|
161
|
+
self.conv_main = nn.Sequential(
|
|
162
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False),
|
|
163
|
+
nn.BatchNorm2d(out_channels),
|
|
164
|
+
nn.SiLU(inplace=True)
|
|
165
|
+
)
|
|
166
|
+
lap = torch.tensor([[0., -1., 0.], [-1., 4., -1.], [0., -1., 0.]]).view(1, 1, 3, 3)
|
|
167
|
+
self.register_buffer('hp_kernel', lap.repeat(in_channels, 1, 1, 1))
|
|
168
|
+
self.hp_proj = nn.Sequential(
|
|
169
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
|
|
170
|
+
nn.BatchNorm2d(out_channels)
|
|
171
|
+
)
|
|
172
|
+
self.gamma = nn.Parameter(torch.tensor([0.15]))
|
|
173
|
+
|
|
174
|
+
def forward(self, x):
|
|
175
|
+
main_feat = self.conv_main(x)
|
|
176
|
+
hp = F.conv2d(x, self.hp_kernel, padding=1, groups=x.shape[1])
|
|
177
|
+
out = main_feat + self.gamma * self.hp_proj(hp)
|
|
178
|
+
if torch.is_autocast_enabled():
|
|
179
|
+
out = out.to(torch.float16)
|
|
180
|
+
return out
|
|
181
|
+
|
|
182
|
+
class SVTRLocalBlock(nn.Module):
|
|
183
|
+
def __init__(self, in_channels, out_channels, stride=1):
|
|
184
|
+
super().__init__()
|
|
185
|
+
self.pw1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
|
|
186
|
+
self.bn1 = nn.BatchNorm2d(out_channels)
|
|
187
|
+
self.act1 = nn.SiLU(inplace=True)
|
|
188
|
+
self.dw = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, groups=out_channels, bias=False)
|
|
189
|
+
self.bn2 = nn.BatchNorm2d(out_channels)
|
|
190
|
+
self.act2 = nn.SiLU(inplace=True)
|
|
191
|
+
self.se = SqueezeExcitation(out_channels, reduction=max(4, out_channels // 16))
|
|
192
|
+
self.pw2 = nn.Conv2d(out_channels, out_channels, kernel_size=1, bias=False)
|
|
193
|
+
self.bn3 = nn.BatchNorm2d(out_channels)
|
|
194
|
+
self.shortcut = nn.Sequential()
|
|
195
|
+
if stride != 1 or in_channels != out_channels:
|
|
196
|
+
self.shortcut = nn.Sequential(
|
|
197
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
|
|
198
|
+
nn.BatchNorm2d(out_channels)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def forward(self, x):
|
|
202
|
+
res = self.shortcut(x)
|
|
203
|
+
out = self.act1(self.bn1(self.pw1(x)))
|
|
204
|
+
out = self.act2(self.bn2(self.dw(out)))
|
|
205
|
+
out = self.se(out)
|
|
206
|
+
out = self.bn3(self.pw2(out))
|
|
207
|
+
if torch.is_autocast_enabled():
|
|
208
|
+
out = out.to(torch.float16)
|
|
209
|
+
res = res.to(torch.float16)
|
|
210
|
+
return F.silu(out + res)
|
|
211
|
+
|
|
212
|
+
class AsterResNetBackbone(nn.Module):
|
|
213
|
+
def __init__(self, in_channels=3, out_dim=512):
|
|
214
|
+
super().__init__()
|
|
215
|
+
self.stem = HighFrequencyStem(in_channels, 64)
|
|
216
|
+
self.layer1 = nn.Sequential(SVTRLocalBlock(64, 64), SVTRLocalBlock(64, 128, stride=(2, 1))) # H: 32 -> 16
|
|
217
|
+
self.layer2 = nn.Sequential(SVTRLocalBlock(128, 128), SVTRLocalBlock(128, 256, stride=(2, 2))) # H: 16 -> 8, W/2
|
|
218
|
+
self.layer3 = nn.Sequential(SVTRLocalBlock(256, 256), SVTRLocalBlock(256, 512, stride=(2, 2))) # H: 8 -> 4, W/4
|
|
219
|
+
self.layer4 = nn.Sequential(
|
|
220
|
+
SVTRLocalBlock(512, 512),
|
|
221
|
+
nn.Conv2d(512, out_dim, kernel_size=(4, 1), stride=(4, 1), bias=False), # H: 4 -> 1, W/4
|
|
222
|
+
nn.BatchNorm2d(out_dim),
|
|
223
|
+
nn.SiLU(inplace=True)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def forward(self, x):
|
|
227
|
+
x = self.stem(x)
|
|
228
|
+
x = self.layer1(x)
|
|
229
|
+
x = self.layer2(x)
|
|
230
|
+
x = self.layer3(x)
|
|
231
|
+
x = self.layer4(x)
|
|
232
|
+
feats = x
|
|
233
|
+
# [B, 512, 1, W/4] -> [B, 512, W/4]
|
|
234
|
+
feats = feats.squeeze(2)
|
|
235
|
+
# [B, 512, W/4] -> [W/4, B, 512]
|
|
236
|
+
feats = feats.permute(2, 0, 1)
|
|
237
|
+
if torch.is_autocast_enabled():
|
|
238
|
+
feats = feats.to(torch.float16)
|
|
239
|
+
return feats
|
|
240
|
+
|
|
241
|
+
class AsterBiLSTMSequence(nn.Module):
|
|
242
|
+
def __init__(self, in_dim=512, hidden_dim=256, num_layers=2):
|
|
243
|
+
super().__init__()
|
|
244
|
+
self.lstm = nn.LSTM(in_dim, hidden_dim, num_layers=num_layers, bidirectional=True, dropout=0.1)
|
|
245
|
+
def forward(self, x):
|
|
246
|
+
out, _ = self.lstm(x)
|
|
247
|
+
return out
|
|
248
|
+
|
|
249
|
+
class PositionalEncoding(nn.Module):
|
|
250
|
+
def __init__(self, d_model=512, max_len=256, dropout=0.1):
|
|
251
|
+
super().__init__()
|
|
252
|
+
self.dropout = nn.Dropout(p=dropout)
|
|
253
|
+
pe = torch.zeros(max_len, d_model)
|
|
254
|
+
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
|
255
|
+
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
|
256
|
+
pe[:, 0::2] = torch.sin(position * div_term)
|
|
257
|
+
pe[:, 1::2] = torch.cos(position * div_term)
|
|
258
|
+
pe = pe.unsqueeze(1)
|
|
259
|
+
self.register_buffer('pe', pe)
|
|
260
|
+
|
|
261
|
+
def forward(self, x):
|
|
262
|
+
seq_len = x.size(0)
|
|
263
|
+
if seq_len > self.pe.size(0):
|
|
264
|
+
# Dynamic computation for extra-long sequences beyond max_len
|
|
265
|
+
position = torch.arange(0, seq_len, dtype=torch.float, device=x.device).unsqueeze(1)
|
|
266
|
+
d_model = x.size(-1)
|
|
267
|
+
div_term = torch.exp(torch.arange(0, d_model, 2, device=x.device).float() * (-math.log(10000.0) / d_model))
|
|
268
|
+
pe = torch.zeros(seq_len, d_model, device=x.device)
|
|
269
|
+
pe[:, 0::2] = torch.sin(position * div_term)
|
|
270
|
+
pe[:, 1::2] = torch.cos(position * div_term)
|
|
271
|
+
pe = pe.unsqueeze(1)
|
|
272
|
+
x = x + pe
|
|
273
|
+
else:
|
|
274
|
+
x = x + self.pe[:seq_len]
|
|
275
|
+
return self.dropout(x)
|
|
276
|
+
|
|
277
|
+
class AsterLanguageModelHead(nn.Module):
|
|
278
|
+
"""Implicit Language Modeling Head: Fast Pre-LN Transformer Encoder."""
|
|
279
|
+
def __init__(self, d_model=512, nhead=4, num_layers=2, dim_feedforward=1024, dropout=0.1):
|
|
280
|
+
super().__init__()
|
|
281
|
+
self.pos_encoder = PositionalEncoding(d_model=d_model, max_len=256, dropout=dropout)
|
|
282
|
+
encoder_layer = nn.TransformerEncoderLayer(
|
|
283
|
+
d_model=d_model,
|
|
284
|
+
nhead=nhead,
|
|
285
|
+
dim_feedforward=dim_feedforward,
|
|
286
|
+
dropout=dropout,
|
|
287
|
+
activation="gelu",
|
|
288
|
+
norm_first=True,
|
|
289
|
+
batch_first=False
|
|
290
|
+
)
|
|
291
|
+
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
|
292
|
+
self.norm = nn.LayerNorm(d_model)
|
|
293
|
+
|
|
294
|
+
def forward(self, x):
|
|
295
|
+
pos_x = self.pos_encoder(x)
|
|
296
|
+
lm_feat = self.transformer(pos_x)
|
|
297
|
+
return self.norm(lm_feat)
|
|
298
|
+
|
|
299
|
+
class GatedLanguageFusion(nn.Module):
|
|
300
|
+
"""Adaptive Gated Vision-Language Fusion."""
|
|
301
|
+
def __init__(self, d_model=512, init_vision_bias=1.5):
|
|
302
|
+
super().__init__()
|
|
303
|
+
self.gate_linear = nn.Linear(d_model * 2, d_model)
|
|
304
|
+
nn.init.constant_(self.gate_linear.bias, init_vision_bias)
|
|
305
|
+
self.sigmoid = nn.Sigmoid()
|
|
306
|
+
|
|
307
|
+
def forward(self, h_vis, h_lang):
|
|
308
|
+
concat = torch.cat([h_vis, h_lang], dim=-1)
|
|
309
|
+
gate = self.sigmoid(self.gate_linear(concat))
|
|
310
|
+
h_fused = gate * h_vis + (1.0 - gate) * h_lang
|
|
311
|
+
return h_fused, gate
|
|
312
|
+
|
|
313
|
+
class AsterNet(nn.Module):
|
|
314
|
+
def __init__(self, num_classes=len(VOCAB_CHARS), feat_dim=512, hidden_dim=256, use_ilm=True):
|
|
315
|
+
super().__init__()
|
|
316
|
+
self.use_ilm = use_ilm
|
|
317
|
+
self.backbone = AsterResNetBackbone(in_channels=3, out_dim=feat_dim)
|
|
318
|
+
self.sequence = AsterBiLSTMSequence(in_dim=feat_dim, hidden_dim=hidden_dim)
|
|
319
|
+
seq_dim = hidden_dim * 2 # 512
|
|
320
|
+
|
|
321
|
+
if self.use_ilm:
|
|
322
|
+
self.language_head = AsterLanguageModelHead(d_model=seq_dim, nhead=4, num_layers=2, dim_feedforward=1024)
|
|
323
|
+
self.fusion = GatedLanguageFusion(d_model=seq_dim, init_vision_bias=1.5)
|
|
324
|
+
|
|
325
|
+
self.classifier = nn.Linear(seq_dim, num_classes)
|
|
326
|
+
self.log_softmax = nn.LogSoftmax(dim=2)
|
|
327
|
+
|
|
328
|
+
def forward(self, x):
|
|
329
|
+
feats = self.backbone(x)
|
|
330
|
+
h_vis = self.sequence(feats)
|
|
331
|
+
|
|
332
|
+
if self.use_ilm:
|
|
333
|
+
h_lang = self.language_head(h_vis)
|
|
334
|
+
h_fused, _ = self.fusion(h_vis, h_lang)
|
|
335
|
+
logits = self.classifier(h_fused)
|
|
336
|
+
else:
|
|
337
|
+
logits = self.classifier(h_vis)
|
|
338
|
+
|
|
339
|
+
return self.log_softmax(logits)
|
|
340
|
+
|
|
341
|
+
def load_matching_state_dict(model, state_dict):
|
|
342
|
+
"""
|
|
343
|
+
Robust state_dict loader that matches tensors by name and exact shape.
|
|
344
|
+
Never crashes on architectural extensions or mismatched layers;
|
|
345
|
+
loads all matching weights and logs any incompatible ones.
|
|
346
|
+
"""
|
|
347
|
+
model_state = model.state_dict()
|
|
348
|
+
matched = {}
|
|
349
|
+
mismatched = []
|
|
350
|
+
missing = []
|
|
351
|
+
|
|
352
|
+
for k, v in state_dict.items():
|
|
353
|
+
if k in model_state:
|
|
354
|
+
if model_state[k].shape == v.shape:
|
|
355
|
+
matched[k] = v
|
|
356
|
+
else:
|
|
357
|
+
mismatched.append(f"{k} (ckpt {tuple(v.shape)} vs model {tuple(model_state[k].shape)})")
|
|
358
|
+
else:
|
|
359
|
+
missing.append(k)
|
|
360
|
+
|
|
361
|
+
model_state.update(matched)
|
|
362
|
+
model.load_state_dict(model_state)
|
|
363
|
+
|
|
364
|
+
if mismatched:
|
|
365
|
+
print(f"[Model Loader] Mismatched shapes ({len(mismatched)} layers skipped): {mismatched[:3]}")
|
|
366
|
+
if missing:
|
|
367
|
+
print(f"[Model Loader] Extra layers in ckpt ({len(missing)}): {missing[:3]}")
|
|
368
|
+
print(f"[Model Loader] Loaded {len(matched)}/{len(model_state)} model layers successfully!")
|
|
369
|
+
return len(matched) > 0
|
|
370
|
+
|
|
371
|
+
# =========================================================================
|
|
372
|
+
# 4. CHUNK UNPACKER & IN-MEMORY DATASET
|
|
373
|
+
# =========================================================================
|
|
374
|
+
class UnpackedChunkDataset(Dataset):
|
|
375
|
+
def __init__(self, raw_data, target_height=32):
|
|
376
|
+
self.samples = []
|
|
377
|
+
self.target_height = target_height
|
|
378
|
+
|
|
379
|
+
if isinstance(raw_data, str):
|
|
380
|
+
with open(raw_data, "rb") as f:
|
|
381
|
+
bio = io.BytesIO(f.read())
|
|
382
|
+
elif isinstance(raw_data, (bytes, bytearray)):
|
|
383
|
+
bio = io.BytesIO(raw_data)
|
|
384
|
+
elif hasattr(raw_data, "read"):
|
|
385
|
+
bio = raw_data
|
|
386
|
+
else:
|
|
387
|
+
raise TypeError(f"Unsupported data type for UnpackedChunkDataset: {type(raw_data)}")
|
|
388
|
+
|
|
389
|
+
magic = bio.read(10)
|
|
390
|
+
if magic != b"ASTCHUNKV1":
|
|
391
|
+
raise ValueError(f"Invalid magic header in chunk stream: {magic}")
|
|
392
|
+
|
|
393
|
+
chunk_idx, num_samples = struct.unpack(">II", bio.read(8))
|
|
394
|
+
|
|
395
|
+
for _ in range(num_samples):
|
|
396
|
+
lbl_len = struct.unpack(">H", bio.read(2))[0]
|
|
397
|
+
lbl = bio.read(lbl_len).decode("utf-8", errors="ignore").strip()
|
|
398
|
+
img_len = struct.unpack(">I", bio.read(4))[0]
|
|
399
|
+
img_b = bio.read(img_len)
|
|
400
|
+
self.samples.append((img_b, lbl if lbl else " "))
|
|
401
|
+
|
|
402
|
+
def __len__(self):
|
|
403
|
+
return len(self.samples)
|
|
404
|
+
|
|
405
|
+
def __getitem__(self, idx):
|
|
406
|
+
img_b, text = self.samples[idx]
|
|
407
|
+
try:
|
|
408
|
+
if not img_b:
|
|
409
|
+
img = Image.new("RGB", (64, self.target_height), (255, 255, 255))
|
|
410
|
+
else:
|
|
411
|
+
img = Image.open(io.BytesIO(img_b)).convert("RGB")
|
|
412
|
+
except Exception:
|
|
413
|
+
img = Image.new("RGB", (64, self.target_height), (255, 255, 255))
|
|
414
|
+
|
|
415
|
+
w, h = img.size
|
|
416
|
+
# Scaled width preserving natural font aspect ratio
|
|
417
|
+
natural_w = max(16, int((float(self.target_height) / max(1, h)) * w))
|
|
418
|
+
|
|
419
|
+
# CTC Sequence Length Guarantee:
|
|
420
|
+
# Downsampling is 4x. Each character requires at least 4 pixels + safety boundary for CTC blanks.
|
|
421
|
+
norm_t = normalize_text(text)
|
|
422
|
+
min_ctc_w = max(16, len(norm_t) * 4 + 16)
|
|
423
|
+
|
|
424
|
+
# Ensure image is wide enough so text is never truncated
|
|
425
|
+
target_w = max(natural_w, min_ctc_w)
|
|
426
|
+
# Cap at 512px (supports up to ~120 characters in a single line)
|
|
427
|
+
new_w = min(512, target_w)
|
|
428
|
+
img = img.resize((new_w, self.target_height), Image.BILINEAR)
|
|
429
|
+
|
|
430
|
+
arr = np.array(img, dtype=np.float32).transpose(2, 0, 1) / 255.0
|
|
431
|
+
return torch.from_numpy(arr), text
|
|
432
|
+
|
|
433
|
+
def chunk_collate_fn(batch):
|
|
434
|
+
if not batch:
|
|
435
|
+
return None
|
|
436
|
+
imgs, texts = zip(*batch)
|
|
437
|
+
# Dynamic batch padding for GPU, Static for TPU
|
|
438
|
+
max_w = max(img.shape[2] for img in imgs)
|
|
439
|
+
pad_w = min(512, max(128, ((max_w + 31) // 32) * 32))
|
|
440
|
+
|
|
441
|
+
# [CRITICAL FIX] TPU (XLA) requires STRICTLY STATIC shapes.
|
|
442
|
+
# If the width changes across batches, TPU will recompile the graph EVERY SINGLE TIME (taking 30+ mins).
|
|
443
|
+
# Force max width (512) for TPU to ensure only ONE compilation happens!
|
|
444
|
+
if getattr(sys.modules[__name__], 'IS_TPU_DEVICE', False):
|
|
445
|
+
pad_w = 512
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
padded_imgs = []
|
|
449
|
+
for img in imgs:
|
|
450
|
+
c, h, w = img.shape
|
|
451
|
+
if w < pad_w:
|
|
452
|
+
pad = torch.ones((c, h, pad_w - w), dtype=torch.float32) # Pure white background padding on right
|
|
453
|
+
padded = torch.cat([img, pad], dim=2)
|
|
454
|
+
elif w > pad_w:
|
|
455
|
+
padded = img[:, :, :pad_w].clone()
|
|
456
|
+
else:
|
|
457
|
+
padded = img.clone()
|
|
458
|
+
padded_imgs.append(padded)
|
|
459
|
+
|
|
460
|
+
stacked_imgs = torch.stack(padded_imgs, dim=0).contiguous()
|
|
461
|
+
|
|
462
|
+
# Sequence length T = pad_w // 4
|
|
463
|
+
max_allowed = (pad_w // 4) - 2
|
|
464
|
+
|
|
465
|
+
is_tpu = getattr(sys.modules[__name__], 'IS_TPU_DEVICE', False)
|
|
466
|
+
|
|
467
|
+
encoded_targets = []
|
|
468
|
+
target_lengths = []
|
|
469
|
+
|
|
470
|
+
if is_tpu:
|
|
471
|
+
# XLA requires strictly static shapes! Pad all targets to max_allowed (126)
|
|
472
|
+
# to prevent the `targets` tensor from changing shape across batches.
|
|
473
|
+
for t in texts:
|
|
474
|
+
enc = encode_text(t)
|
|
475
|
+
if not enc:
|
|
476
|
+
enc = [1]
|
|
477
|
+
if len(enc) > max_allowed:
|
|
478
|
+
enc = enc[:max_allowed]
|
|
479
|
+
target_lengths.append(len(enc))
|
|
480
|
+
# Pad to max_allowed with BLANK_IDX (0)
|
|
481
|
+
enc = enc + [0] * (max_allowed - len(enc))
|
|
482
|
+
encoded_targets.append(enc)
|
|
483
|
+
targets_tensor = torch.tensor(encoded_targets, dtype=torch.long)
|
|
484
|
+
else:
|
|
485
|
+
for t in texts:
|
|
486
|
+
enc = encode_text(t)
|
|
487
|
+
if not enc:
|
|
488
|
+
enc = [1]
|
|
489
|
+
if len(enc) > max_allowed:
|
|
490
|
+
enc = enc[:max_allowed]
|
|
491
|
+
encoded_targets.extend(enc)
|
|
492
|
+
target_lengths.append(len(enc))
|
|
493
|
+
targets_tensor = torch.tensor(encoded_targets, dtype=torch.long)
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
"images": stacked_imgs,
|
|
497
|
+
"texts": list(texts),
|
|
498
|
+
"targets": targets_tensor,
|
|
499
|
+
"target_lengths": torch.tensor(target_lengths, dtype=torch.long)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
def greedy_decode(log_probs):
|
|
503
|
+
probs = torch.exp(log_probs)
|
|
504
|
+
best_indices = torch.argmax(probs, dim=2).detach().cpu().numpy() # (T, B)
|
|
505
|
+
T, B = best_indices.shape
|
|
506
|
+
res = []
|
|
507
|
+
for b in range(B):
|
|
508
|
+
seq = best_indices[:, b]
|
|
509
|
+
chars = []
|
|
510
|
+
prev = None
|
|
511
|
+
for idx in seq:
|
|
512
|
+
if idx != prev:
|
|
513
|
+
if idx != BLANK_IDX:
|
|
514
|
+
chars.append(VOCAB_CHARS[idx])
|
|
515
|
+
prev = idx
|
|
516
|
+
res.append("".join(chars))
|
|
517
|
+
return res
|
|
518
|
+
|
|
519
|
+
_cached_tunnel_url = None
|
|
520
|
+
_last_tunnel_check = 0.0
|
|
521
|
+
|
|
522
|
+
def resolve_current_tunnel_url(default_url):
|
|
523
|
+
"""
|
|
524
|
+
Dynamically resolves active tunnel URL with 30-second TTL cache:
|
|
525
|
+
1. Checks cache first to avoid blocking training loop with network requests
|
|
526
|
+
2. Checks Cloud auto-discovery channel (ntfy.sh/asterocr_tunnel_creyt2012)
|
|
527
|
+
3. Checks local file /kaggle/working/tunnel_url.txt or environment variable
|
|
528
|
+
4. Fallback to injected default_url
|
|
529
|
+
"""
|
|
530
|
+
global _cached_tunnel_url, _last_tunnel_check
|
|
531
|
+
now = time.time()
|
|
532
|
+
if _cached_tunnel_url and (now - _last_tunnel_check < 30.0):
|
|
533
|
+
return _cached_tunnel_url
|
|
534
|
+
|
|
535
|
+
resolved = None
|
|
536
|
+
# 1. Zero-Touch Cloud Auto-Discovery
|
|
537
|
+
try:
|
|
538
|
+
r = requests.get("https://ntfy.sh/asterocr_tunnel_creyt2012/json?poll=1", timeout=3)
|
|
539
|
+
if r.status_code == 200:
|
|
540
|
+
urls = []
|
|
541
|
+
for line in r.text.strip().splitlines():
|
|
542
|
+
if not line.strip():
|
|
543
|
+
continue
|
|
544
|
+
try:
|
|
545
|
+
data = json.loads(line)
|
|
546
|
+
msg = data.get("message", "").strip()
|
|
547
|
+
if msg.startswith("http://") or msg.startswith("https://"):
|
|
548
|
+
urls.append(msg)
|
|
549
|
+
except Exception:
|
|
550
|
+
pass
|
|
551
|
+
if urls:
|
|
552
|
+
resolved = urls[-1].rstrip("/")
|
|
553
|
+
except Exception:
|
|
554
|
+
pass
|
|
555
|
+
|
|
556
|
+
if not resolved:
|
|
557
|
+
# 2. Local environment or file override
|
|
558
|
+
env_u = os.environ.get("ASTER_TUNNEL_URL", "").strip()
|
|
559
|
+
if env_u.startswith("http://") or env_u.startswith("https://"):
|
|
560
|
+
resolved = env_u.rstrip("/")
|
|
561
|
+
else:
|
|
562
|
+
for p in ["/kaggle/working/tunnel_url.txt", "tunnel_url.txt"]:
|
|
563
|
+
if os.path.exists(p):
|
|
564
|
+
try:
|
|
565
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
566
|
+
u = f.read().strip()
|
|
567
|
+
if u.startswith("http://") or u.startswith("https://"):
|
|
568
|
+
resolved = u.rstrip("/")
|
|
569
|
+
break
|
|
570
|
+
except Exception:
|
|
571
|
+
pass
|
|
572
|
+
|
|
573
|
+
if not resolved:
|
|
574
|
+
resolved = default_url.rstrip("/")
|
|
575
|
+
|
|
576
|
+
_cached_tunnel_url = resolved
|
|
577
|
+
_last_tunnel_check = now
|
|
578
|
+
return resolved
|
|
579
|
+
|
|
580
|
+
# =========================================================================
|
|
581
|
+
# 5. ASYNC PIPELINE EXECUTORS & CONTINUOUS CHUNK STREAMER (RING BUFFER)
|
|
582
|
+
# =========================================================================
|
|
583
|
+
telemetry_pool = concurrent.futures.ThreadPoolExecutor(max_workers=3)
|
|
584
|
+
|
|
585
|
+
def async_post_telemetry(url, payload, imgs_cpu=None, texts=None, p_texts=None):
|
|
586
|
+
try:
|
|
587
|
+
if imgs_cpu is not None and texts is not None and p_texts is not None:
|
|
588
|
+
sample_preds = []
|
|
589
|
+
for pi, pt in enumerate(p_texts):
|
|
590
|
+
tr = texts[pi]
|
|
591
|
+
b64_str = ""
|
|
592
|
+
try:
|
|
593
|
+
arr = (imgs_cpu[pi].transpose(1, 2, 0) * 255.0).clip(0, 255).astype(np.uint8)
|
|
594
|
+
pim = Image.fromarray(arr)
|
|
595
|
+
pbuf = io.BytesIO()
|
|
596
|
+
pim.save(pbuf, format="JPEG", quality=65)
|
|
597
|
+
b64_str = "data:image/jpeg;base64," + base64.b64encode(pbuf.getvalue()).decode("utf-8")
|
|
598
|
+
except Exception:
|
|
599
|
+
pass
|
|
600
|
+
sample_preds.append({
|
|
601
|
+
"truth": tr,
|
|
602
|
+
"pred": pt,
|
|
603
|
+
"correct": (tr.strip() == pt.strip()),
|
|
604
|
+
"image_b64": b64_str
|
|
605
|
+
})
|
|
606
|
+
payload["sample_predictions"] = sample_preds
|
|
607
|
+
requests.post(f"{url}/api/kaggle/stream/telemetry", json=payload, timeout=8)
|
|
608
|
+
except Exception:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
def async_save_checkpoint(url, session_token, step, chunk_idx, state_dict, vocab, loss_val, best_loss, num_gpus=2):
|
|
612
|
+
try:
|
|
613
|
+
# Pre-check: Reject if any tensor in state_dict contains NaN or Inf
|
|
614
|
+
for k, v in state_dict.items():
|
|
615
|
+
if torch.is_tensor(v) and (torch.isnan(v).any() or torch.isinf(v).any()):
|
|
616
|
+
print(f"[!] Safety Block: state_dict contains NaN in '{k}'! Checkpoint upload aborted.")
|
|
617
|
+
return
|
|
618
|
+
buf = io.BytesIO()
|
|
619
|
+
torch.save({
|
|
620
|
+
"step": step,
|
|
621
|
+
"chunk_idx": chunk_idx,
|
|
622
|
+
"state_dict": state_dict,
|
|
623
|
+
"vocab": vocab,
|
|
624
|
+
"loss": loss_val,
|
|
625
|
+
"best_loss": best_loss
|
|
626
|
+
}, buf)
|
|
627
|
+
buf.seek(0)
|
|
628
|
+
requests.post(
|
|
629
|
+
f"{url}/api/kaggle/stream/save_checkpoint",
|
|
630
|
+
files={"checkpoint": ("asternet_last.pth", buf, "application/octet-stream")},
|
|
631
|
+
data={
|
|
632
|
+
"token": session_token,
|
|
633
|
+
"step": step,
|
|
634
|
+
"chunk_idx": chunk_idx,
|
|
635
|
+
"loss": loss_val,
|
|
636
|
+
"best_loss": best_loss,
|
|
637
|
+
"num_gpus": num_gpus
|
|
638
|
+
},
|
|
639
|
+
timeout=25
|
|
640
|
+
)
|
|
641
|
+
except Exception:
|
|
642
|
+
pass
|
|
643
|
+
|
|
644
|
+
# Buffer holds up to 15 chunks in RAM (~135MB total - eliminates Cloudflare tunnel wait stalls!)
|
|
645
|
+
BUFFER_CHUNKS = 15
|
|
646
|
+
chunk_queue = queue.Queue(maxsize=BUFFER_CHUNKS)
|
|
647
|
+
streamer_stop_event = threading.Event()
|
|
648
|
+
|
|
649
|
+
def background_chunk_producer(active_url_getter, session_token, start_chunk_idx, stop_event, shared_dir=None):
|
|
650
|
+
"""
|
|
651
|
+
Dedicated Producer Thread:
|
|
652
|
+
Continuously pulls chunks from local machine via Tunnel.
|
|
653
|
+
- If shared_dir is provided (DDP mode): writes raw chunk to RAM disk (e.g. /dev/shm)
|
|
654
|
+
and queues the filepath so all DDP processes can open it without IPC overhead.
|
|
655
|
+
- If shared_dir is None (Single-GPU mode): unpacks dataset directly in RAM.
|
|
656
|
+
"""
|
|
657
|
+
c_idx = start_chunk_idx
|
|
658
|
+
offline_logged = False
|
|
659
|
+
cur_epoch = 1
|
|
660
|
+
tot_epochs = MAX_EPOCHS
|
|
661
|
+
|
|
662
|
+
while not stop_event.is_set():
|
|
663
|
+
current_url = active_url_getter()
|
|
664
|
+
# Check remote stop signal periodically (every 5 chunks to avoid roundtrip overhead)
|
|
665
|
+
if c_idx % 5 == 0:
|
|
666
|
+
try:
|
|
667
|
+
r_ctrl = requests.get(f"{current_url}/api/kaggle/stream/control?token={session_token}", timeout=5)
|
|
668
|
+
if r_ctrl.status_code == 200 and r_ctrl.json().get("should_stop", False):
|
|
669
|
+
print("[!] Stop signal received by Streamer. Stopping producer.")
|
|
670
|
+
chunk_queue.put(None)
|
|
671
|
+
return
|
|
672
|
+
except Exception:
|
|
673
|
+
pass
|
|
674
|
+
|
|
675
|
+
# Fetch chunk bytes with retry
|
|
676
|
+
chunk_bytes = None
|
|
677
|
+
while not stop_event.is_set():
|
|
678
|
+
current_url = active_url_getter()
|
|
679
|
+
try:
|
|
680
|
+
res = requests.get(
|
|
681
|
+
f"{current_url}/api/kaggle/stream/get_chunk",
|
|
682
|
+
params={"token": session_token, "chunk_idx": c_idx},
|
|
683
|
+
timeout=180
|
|
684
|
+
)
|
|
685
|
+
if res.status_code == 200 and len(res.content) >= 32:
|
|
686
|
+
chunk_bytes = res.content
|
|
687
|
+
try:
|
|
688
|
+
cur_epoch = int(res.headers.get("X-Current-Epoch", cur_epoch))
|
|
689
|
+
tot_epochs = int(res.headers.get("X-Total-Epochs", tot_epochs))
|
|
690
|
+
except Exception:
|
|
691
|
+
pass
|
|
692
|
+
if offline_logged:
|
|
693
|
+
print(f"\n[+] Streamer: Đã kết nối lại thành công! Tải xong Chunk #{c_idx}...")
|
|
694
|
+
offline_logged = False
|
|
695
|
+
break
|
|
696
|
+
elif res.status_code == 204:
|
|
697
|
+
print(f"[+] Streamer: Đã nạp hết toàn bộ các chunk dữ liệu ({tot_epochs} Epochs hoàn tất)!")
|
|
698
|
+
chunk_queue.put(None)
|
|
699
|
+
return
|
|
700
|
+
else:
|
|
701
|
+
if not offline_logged:
|
|
702
|
+
print(f"\n[!] Máy local đang Offline (Mã: {res.status_code}). Streamer tự động chờ...")
|
|
703
|
+
offline_logged = True
|
|
704
|
+
time.sleep(5)
|
|
705
|
+
except Exception as net_err:
|
|
706
|
+
if not offline_logged:
|
|
707
|
+
print(f"\n[!] Mất kết nối tới Studio máy local ({net_err}). Streamer tự động chờ...")
|
|
708
|
+
offline_logged = True
|
|
709
|
+
time.sleep(5)
|
|
710
|
+
|
|
711
|
+
if chunk_bytes and not stop_event.is_set():
|
|
712
|
+
try:
|
|
713
|
+
t0 = time.time()
|
|
714
|
+
if shared_dir:
|
|
715
|
+
chunk_filename = f"active_chunk_{c_idx}.bin"
|
|
716
|
+
chunk_path = os.path.join(shared_dir, chunk_filename)
|
|
717
|
+
with open(chunk_path, "wb") as f:
|
|
718
|
+
f.write(chunk_bytes)
|
|
719
|
+
dt = time.time() - t0
|
|
720
|
+
mb_size = len(chunk_bytes) / (1024 * 1024)
|
|
721
|
+
print(f"[+] Streamer Pipeline [Epoch {cur_epoch}/{tot_epochs}]: Chunk #{c_idx} ({mb_size:.1f}MB in {dt:.2f}s) -> RAM Disk ({chunk_queue.qsize() + 1}/{BUFFER_CHUNKS})")
|
|
722
|
+
chunk_queue.put((c_idx, chunk_path, cur_epoch, tot_epochs))
|
|
723
|
+
else:
|
|
724
|
+
ds = UnpackedChunkDataset(chunk_bytes, target_height=TARGET_HEIGHT)
|
|
725
|
+
dt = time.time() - t0
|
|
726
|
+
print(f"[+] Streamer Pipeline [Epoch {cur_epoch}/{tot_epochs}]: Chunk #{c_idx} ({len(ds):,} mẫu in {dt:.2f}s) -> RAM Buffer ({chunk_queue.qsize() + 1}/{BUFFER_CHUNKS})")
|
|
727
|
+
chunk_queue.put((c_idx, ds, cur_epoch, tot_epochs))
|
|
728
|
+
c_idx += 1
|
|
729
|
+
except Exception as unpack_err:
|
|
730
|
+
print(f"[!] Lỗi nạp Chunk #{c_idx}: {unpack_err}")
|
|
731
|
+
time.sleep(2)
|
|
732
|
+
|
|
733
|
+
class BucketBatchSampler(torch.utils.data.Sampler):
|
|
734
|
+
"""
|
|
735
|
+
Dynamic Length-Bucket Batch Sampler:
|
|
736
|
+
Groups images of similar text lengths together into the same batch.
|
|
737
|
+
Eliminates up to 70% of redundant white-padding on Tensor Cores!
|
|
738
|
+
Ensures identical batch dimensions across both DDP ranks for perfect AllReduce synchronization.
|
|
739
|
+
"""
|
|
740
|
+
def __init__(self, dataset, batch_size, rank=0, world_size=1, shuffle=True, seed=42, drop_last=False):
|
|
741
|
+
self.dataset = dataset
|
|
742
|
+
self.batch_size = batch_size
|
|
743
|
+
self.rank = rank
|
|
744
|
+
self.world_size = world_size
|
|
745
|
+
self.shuffle = shuffle
|
|
746
|
+
self.seed = seed
|
|
747
|
+
self.epoch = 0
|
|
748
|
+
self.drop_last = drop_last
|
|
749
|
+
|
|
750
|
+
def set_epoch(self, epoch):
|
|
751
|
+
self.epoch = epoch
|
|
752
|
+
|
|
753
|
+
def __iter__(self):
|
|
754
|
+
g = torch.Generator()
|
|
755
|
+
g.manual_seed(self.seed + self.epoch)
|
|
756
|
+
|
|
757
|
+
indices = list(range(len(self.dataset)))
|
|
758
|
+
if self.shuffle:
|
|
759
|
+
indices = torch.randperm(len(self.dataset), generator=g).tolist()
|
|
760
|
+
|
|
761
|
+
sorted_indices = sorted(indices, key=lambda i: len(self.dataset.samples[i][1]))
|
|
762
|
+
|
|
763
|
+
global_batch_size = self.batch_size * self.world_size
|
|
764
|
+
batches = []
|
|
765
|
+
for i in range(0, len(sorted_indices), global_batch_size):
|
|
766
|
+
chunk_batch = sorted_indices[i:i + global_batch_size]
|
|
767
|
+
|
|
768
|
+
if self.drop_last and len(chunk_batch) < global_batch_size:
|
|
769
|
+
continue
|
|
770
|
+
|
|
771
|
+
# Ensure every rank receives at least 1 sample, and evenly distributed
|
|
772
|
+
per_rank = len(chunk_batch) // self.world_size
|
|
773
|
+
if per_rank > 0:
|
|
774
|
+
even_len = per_rank * self.world_size
|
|
775
|
+
batches.append((chunk_batch[:even_len], per_rank))
|
|
776
|
+
|
|
777
|
+
if self.shuffle:
|
|
778
|
+
perm = torch.randperm(len(batches), generator=g).tolist()
|
|
779
|
+
batches = [batches[p] for p in perm]
|
|
780
|
+
|
|
781
|
+
for b, per_rank in batches:
|
|
782
|
+
rank_start = self.rank * per_rank
|
|
783
|
+
rank_end = rank_start + per_rank
|
|
784
|
+
batch_slice = b[rank_start:rank_end]
|
|
785
|
+
if len(batch_slice) > 0:
|
|
786
|
+
yield batch_slice
|
|
787
|
+
|
|
788
|
+
def __len__(self):
|
|
789
|
+
global_batch_size = self.batch_size * self.world_size
|
|
790
|
+
return (len(self.dataset) + global_batch_size - 1) // global_batch_size
|
|
791
|
+
|
|
792
|
+
# =========================================================================
|
|
793
|
+
# 6. DISTRIBUTED DATA PARALLEL (DDP) WORKER FOR MULTI-GPU (DUAL TESLA T4)
|
|
794
|
+
# =========================================================================
|
|
795
|
+
def ddp_train_worker(rank, world_size):
|
|
796
|
+
"""
|
|
797
|
+
Dedicated Multi-Process DDP Worker.
|
|
798
|
+
Each GPU has its own isolated process, CUDA context, and memory space.
|
|
799
|
+
Eliminates cuDNN multi-threading collisions across virtualized PCIe bus!
|
|
800
|
+
"""
|
|
801
|
+
os.environ["MASTER_ADDR"] = "127.0.0.1"
|
|
802
|
+
os.environ["MASTER_PORT"] = "29500"
|
|
803
|
+
os.environ["NCCL_P2P_DISABLE"] = "1" # Required for Kaggle virtualized cloud GPUs (no NVLink)
|
|
804
|
+
os.environ["NCCL_IB_DISABLE"] = "1"
|
|
805
|
+
|
|
806
|
+
torch.cuda.set_device(rank)
|
|
807
|
+
device = torch.device(f"cuda:{rank}")
|
|
808
|
+
dist.init_process_group(backend="nccl", rank=rank, world_size=world_size, device_id=device)
|
|
809
|
+
|
|
810
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
811
|
+
|
|
812
|
+
if rank == 0:
|
|
813
|
+
print("=" * 70)
|
|
814
|
+
print(" AsterOCR Cloud GPU Worker (Dual NVIDIA Tesla T4 x2 - Multi-Process DDP)")
|
|
815
|
+
print("=" * 70)
|
|
816
|
+
print(f"[+] Multi-Process Cluster: 2x Independent Processes (Rank 0 on GPU #0, Rank 1 on GPU #1)")
|
|
817
|
+
print(f"[+] Đồng bộ Gradient : NCCL AllReduce (Shared Memory Transport - Zero Race Condition!)")
|
|
818
|
+
print(f"[+] Tải huấn luyện : {BATCH_SIZE} mẫu/GPU -> Tổng thông lượng {BATCH_SIZE * world_size} mẫu/batch (Mục tiêu ~1,500 sps)")
|
|
819
|
+
print(f"[+] Tunnel Endpoint : {active_url}")
|
|
820
|
+
try:
|
|
821
|
+
requests.post(f"{active_url}/api/kaggle/stream/heartbeat", json={
|
|
822
|
+
"device_name": "Dual NVIDIA Tesla T4 x2 (DDP Cluster - 2x Processes)",
|
|
823
|
+
"vram_mb": 30000.0,
|
|
824
|
+
"status": "online"
|
|
825
|
+
}, timeout=10)
|
|
826
|
+
except Exception:
|
|
827
|
+
pass
|
|
828
|
+
|
|
829
|
+
# 1. Initialize AsterNet model on local GPU
|
|
830
|
+
base_model = AsterNet(num_classes=len(VOCAB_CHARS)).to(device)
|
|
831
|
+
|
|
832
|
+
# 2. Rank 0 restores checkpoint if available (Bundled weights first, then server)
|
|
833
|
+
chunk_idx = 0
|
|
834
|
+
global_step = 0
|
|
835
|
+
best_loss = 999.0
|
|
836
|
+
loaded_ckpt = False
|
|
837
|
+
|
|
838
|
+
if rank == 0:
|
|
839
|
+
# Step A: Check for pre-bundled weights.pth inside the kernel package (Instant recovery, zero network lag!)
|
|
840
|
+
candidate_weights = ["weights.pth", "/kaggle/working/weights.pth", "/kaggle/src/weights.pth"]
|
|
841
|
+
for cw in candidate_weights:
|
|
842
|
+
if os.path.exists(cw) and os.path.getsize(cw) > 1000:
|
|
843
|
+
try:
|
|
844
|
+
ckpt = torch.load(cw, map_location=device)
|
|
845
|
+
state_d = ckpt.get("state_dict", ckpt)
|
|
846
|
+
load_matching_state_dict(base_model, state_d)
|
|
847
|
+
if isinstance(ckpt, dict):
|
|
848
|
+
global_step = int(ckpt.get("step", 0))
|
|
849
|
+
chunk_idx = int(ckpt.get("chunk_idx", 0))
|
|
850
|
+
best_loss = float(ckpt.get("best_loss", ckpt.get("loss", 999.0)))
|
|
851
|
+
print(f"[Rank 0] Phục hồi Checkpoint đóng gói THÀNH CÔNG từ {cw}! Tiếp tục từ Step {global_step:,}, Chunk #{chunk_idx:,}, Best Loss {best_loss:.4f}")
|
|
852
|
+
loaded_ckpt = True
|
|
853
|
+
break
|
|
854
|
+
except Exception as ex:
|
|
855
|
+
print(f"[Rank 0] Cảnh báo nạp weights đóng gói: {ex}")
|
|
856
|
+
|
|
857
|
+
# Step B: Check server via tunnel with generous DNS propagation retry (up to 10 attempts)
|
|
858
|
+
for attempt in range(1, 11):
|
|
859
|
+
try:
|
|
860
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
861
|
+
r_weights = requests.get(f"{active_url}/api/kaggle/stream/init_weights", timeout=15)
|
|
862
|
+
if r_weights.status_code == 200 and len(r_weights.content) > 1000:
|
|
863
|
+
ckpt = torch.load(io.BytesIO(r_weights.content), map_location=device)
|
|
864
|
+
state_d = ckpt.get("state_dict", ckpt)
|
|
865
|
+
server_step = int(ckpt.get("step", 0)) if isinstance(ckpt, dict) else 0
|
|
866
|
+
if server_step >= global_step:
|
|
867
|
+
load_matching_state_dict(base_model, state_d)
|
|
868
|
+
global_step = server_step
|
|
869
|
+
chunk_idx = int(ckpt.get("chunk_idx", 0))
|
|
870
|
+
best_loss = float(ckpt.get("best_loss", ckpt.get("loss", 999.0)))
|
|
871
|
+
print(f"[Rank 0] Checkpoint mới nhất từ Server đồng bộ THÀNH CÔNG! Tiếp tục từ Step {global_step:,}, Chunk #{chunk_idx:,}, Best Loss {best_loss:.4f}")
|
|
872
|
+
loaded_ckpt = True
|
|
873
|
+
break
|
|
874
|
+
elif r_weights.status_code == 204:
|
|
875
|
+
break
|
|
876
|
+
except Exception as e:
|
|
877
|
+
print(f"[Rank 0] Server checkpoint sync attempt {attempt}/10 ({e})")
|
|
878
|
+
time.sleep(4)
|
|
879
|
+
|
|
880
|
+
if not loaded_ckpt:
|
|
881
|
+
print("[Rank 0] Bắt đầu từ trọng số khởi tạo (không tìm thấy checkpoint).")
|
|
882
|
+
|
|
883
|
+
# 3. Wrap with DistributedDataParallel
|
|
884
|
+
# find_unused_parameters=True prevents DDP deadlock with dynamic CTC vocab & conditional gating
|
|
885
|
+
model = nn.parallel.DistributedDataParallel(
|
|
886
|
+
base_model,
|
|
887
|
+
device_ids=[rank],
|
|
888
|
+
output_device=rank,
|
|
889
|
+
find_unused_parameters=True
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
# Broadcast checkpoint step and chunk_idx to all DDP ranks
|
|
893
|
+
sync_tensor = torch.tensor([global_step, chunk_idx, int(min(best_loss, 999.0) * 10000)], dtype=torch.long, device=device)
|
|
894
|
+
dist.broadcast(sync_tensor, src=0)
|
|
895
|
+
global_step = int(sync_tensor[0].item())
|
|
896
|
+
chunk_idx = int(sync_tensor[1].item())
|
|
897
|
+
best_loss = float(sync_tensor[2].item()) / 10000.0
|
|
898
|
+
|
|
899
|
+
session_start_time = time.time()
|
|
900
|
+
session_initial_step = global_step
|
|
901
|
+
|
|
902
|
+
optimizer = torch.optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)
|
|
903
|
+
scaler = torch.amp.GradScaler("cuda")
|
|
904
|
+
torch.backends.cudnn.benchmark = True
|
|
905
|
+
torch.backends.cudnn.deterministic = False
|
|
906
|
+
# zero_infinity=True activates auto-zeroing of unaligned CTC sequences (prevents NaN gradients)
|
|
907
|
+
ctc_criterion = nn.CTCLoss(blank=BLANK_IDX, reduction="mean", zero_infinity=True)
|
|
908
|
+
|
|
909
|
+
shared_dir = "/dev/shm" if os.path.exists("/dev/shm") and os.access("/dev/shm", os.W_OK) else "/kaggle/working/shm"
|
|
910
|
+
os.makedirs(shared_dir, exist_ok=True)
|
|
911
|
+
meta_path = os.path.join(shared_dir, "ddp_chunk_meta.json")
|
|
912
|
+
|
|
913
|
+
# Start dedicated background streamer thread only on Rank 0 from recovered chunk_idx
|
|
914
|
+
if rank == 0:
|
|
915
|
+
get_url_fn = lambda: resolve_current_tunnel_url(TUNNEL_URL)
|
|
916
|
+
streamer_thread = threading.Thread(
|
|
917
|
+
target=background_chunk_producer,
|
|
918
|
+
args=(get_url_fn, SESSION_TOKEN, chunk_idx, streamer_stop_event, shared_dir),
|
|
919
|
+
daemon=True
|
|
920
|
+
)
|
|
921
|
+
streamer_thread.start()
|
|
922
|
+
print(f"[Rank 0] Luồng nạp dữ liệu nạp trực tiếp vào RAM Disk (/dev/shm) từ Chunk #{chunk_idx}!")
|
|
923
|
+
# Pre-buffer at least 4 chunks to build zero-stall pipeline momentum
|
|
924
|
+
wait_t0 = time.time()
|
|
925
|
+
print("[Rank 0] Đang nạp trước 4 chunks vào RAM Disk để GPU chạy liên tục 100% không khựng...")
|
|
926
|
+
while chunk_queue.qsize() < 4 and (time.time() - wait_t0 < 25.0) and not streamer_stop_event.is_set():
|
|
927
|
+
time.sleep(0.3)
|
|
928
|
+
print(f"[Rank 0] Bộ đệm đã sẵn sàng ({chunk_queue.qsize()} chunks trong RAM Disk). Bắt đầu DDP Loop!")
|
|
929
|
+
|
|
930
|
+
# Synchronize all ranks so both GPUs enter training loop together
|
|
931
|
+
dist.barrier()
|
|
932
|
+
|
|
933
|
+
# Real-time wall-clock throughput tracking
|
|
934
|
+
last_step_wall_time = time.time()
|
|
935
|
+
smooth_sps = 0.0
|
|
936
|
+
last_ckpt_time = 0.0
|
|
937
|
+
|
|
938
|
+
# 4. Consumer Training Loop
|
|
939
|
+
while True:
|
|
940
|
+
if rank == 0:
|
|
941
|
+
item = chunk_queue.get()
|
|
942
|
+
if item is None:
|
|
943
|
+
meta = {"done": True, "c_idx": -1, "path": "", "cur_epoch": 0, "tot_epochs": 0}
|
|
944
|
+
else:
|
|
945
|
+
c_idx, path, cur_epoch, tot_epochs = item
|
|
946
|
+
meta = {"done": False, "c_idx": c_idx, "path": path, "cur_epoch": cur_epoch, "tot_epochs": tot_epochs}
|
|
947
|
+
with open(meta_path, "w", encoding="utf-8") as f:
|
|
948
|
+
json.dump(meta, f)
|
|
949
|
+
|
|
950
|
+
# Barrier 1: ensure Rank 0 has written the chunk and metadata before Rank 1 reads
|
|
951
|
+
dist.barrier()
|
|
952
|
+
|
|
953
|
+
with open(meta_path, "r", encoding="utf-8") as f:
|
|
954
|
+
meta = json.load(f)
|
|
955
|
+
|
|
956
|
+
if meta.get("done", False):
|
|
957
|
+
if rank == 0:
|
|
958
|
+
print("[+] Toàn bộ lô dữ liệu đã hoàn thành! Chu kỳ kết thúc!")
|
|
959
|
+
break
|
|
960
|
+
|
|
961
|
+
chunk_idx = meta["c_idx"]
|
|
962
|
+
chunk_path = meta["path"]
|
|
963
|
+
cur_epoch = meta["cur_epoch"]
|
|
964
|
+
tot_epochs = meta["tot_epochs"]
|
|
965
|
+
|
|
966
|
+
cur_lr = compute_scheduled_lr(LEARNING_RATE, cur_epoch, tot_epochs)
|
|
967
|
+
for pg in optimizer.param_groups:
|
|
968
|
+
pg["lr"] = cur_lr
|
|
969
|
+
|
|
970
|
+
if rank == 0:
|
|
971
|
+
print(f"\n[+] GPU Cluster [Epoch {cur_epoch}/{tot_epochs} | Auto-LR: {cur_lr:.6f}]: Bắt đầu train Chunk #{chunk_idx} song song trên CẢ 2x TESLA T4...")
|
|
972
|
+
|
|
973
|
+
dataset = UnpackedChunkDataset(chunk_path, target_height=TARGET_HEIGHT)
|
|
974
|
+
batch_sampler = BucketBatchSampler(
|
|
975
|
+
dataset,
|
|
976
|
+
batch_size=BATCH_SIZE,
|
|
977
|
+
rank=rank,
|
|
978
|
+
world_size=world_size,
|
|
979
|
+
shuffle=True
|
|
980
|
+
)
|
|
981
|
+
batch_sampler.set_epoch(cur_epoch * 1000 + chunk_idx)
|
|
982
|
+
|
|
983
|
+
# In-Memory RAM Dataset: num_workers=0 provides instant zero-delay access (eliminates 2s process-spawn stall!)
|
|
984
|
+
dataloader = DataLoader(
|
|
985
|
+
dataset,
|
|
986
|
+
batch_sampler=batch_sampler,
|
|
987
|
+
collate_fn=chunk_collate_fn,
|
|
988
|
+
num_workers=0,
|
|
989
|
+
pin_memory=True
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
model.train()
|
|
993
|
+
optimizer.zero_grad(set_to_none=True)
|
|
994
|
+
grad_norm = 0.0
|
|
995
|
+
|
|
996
|
+
for batch_idx, batch in enumerate(dataloader):
|
|
997
|
+
if batch is None:
|
|
998
|
+
continue
|
|
999
|
+
t_prep_0 = time.time()
|
|
1000
|
+
imgs = batch["images"].to(device, non_blocking=True)
|
|
1001
|
+
targets = batch["targets"].to(device, non_blocking=True)
|
|
1002
|
+
target_lengths = batch["target_lengths"].to(device, non_blocking=True)
|
|
1003
|
+
input_lengths = torch.full((imgs.size(0),), imgs.size(3) // 4, dtype=torch.long, device=device)
|
|
1004
|
+
t_prep = (time.time() - t_prep_0) * 1000
|
|
1005
|
+
|
|
1006
|
+
t_fwd_0 = time.time()
|
|
1007
|
+
with torch.amp.autocast("cuda", dtype=torch.float16):
|
|
1008
|
+
log_probs = model(imgs) # (T, B, C) Fused Vision-Language Logits
|
|
1009
|
+
t_fwd = (time.time() - t_fwd_0) * 1000
|
|
1010
|
+
|
|
1011
|
+
t_loss_0 = time.time()
|
|
1012
|
+
loss = ctc_criterion(log_probs.float(), targets, input_lengths, target_lengths)
|
|
1013
|
+
t_loss = (time.time() - t_loss_0) * 1000
|
|
1014
|
+
|
|
1015
|
+
is_invalid = torch.isnan(loss) or torch.isinf(loss)
|
|
1016
|
+
if is_invalid:
|
|
1017
|
+
loss = (log_probs * 0.0).sum()
|
|
1018
|
+
|
|
1019
|
+
t_bwd_0 = time.time()
|
|
1020
|
+
scaler.scale(loss).backward() # NCCL automatically triggers all-reduce across BOTH ranks synchronously!
|
|
1021
|
+
t_bwd = (time.time() - t_bwd_0) * 1000
|
|
1022
|
+
|
|
1023
|
+
if not is_invalid:
|
|
1024
|
+
scaler.unscale_(optimizer)
|
|
1025
|
+
grad_norm = float(torch.nn.utils.clip_grad_norm_(base_model.parameters(), max_norm=2.0))
|
|
1026
|
+
scaler.step(optimizer)
|
|
1027
|
+
scaler.update()
|
|
1028
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1029
|
+
global_step += 1
|
|
1030
|
+
|
|
1031
|
+
loss_val = float(loss.item()) if not is_invalid else 999.0
|
|
1032
|
+
|
|
1033
|
+
# Only Rank 0 handles telemetry and checkpoints
|
|
1034
|
+
if rank == 0:
|
|
1035
|
+
now_t = time.time()
|
|
1036
|
+
# Async background checkpoint save: only when breaking loss record or every 100 steps (max once per 45s)
|
|
1037
|
+
should_save_best = (loss_val < (best_loss - 0.003) and (now_t - last_ckpt_time) > 45.0)
|
|
1038
|
+
should_save_periodic = (global_step % 100 == 0 and (now_t - last_ckpt_time) > 45.0)
|
|
1039
|
+
if (should_save_best or should_save_periodic) and global_step > 15:
|
|
1040
|
+
if loss_val < best_loss:
|
|
1041
|
+
best_loss = loss_val
|
|
1042
|
+
last_ckpt_time = now_t
|
|
1043
|
+
sd_copy = {k: v.cpu().clone() for k, v in base_model.state_dict().items()}
|
|
1044
|
+
telemetry_pool.submit(async_save_checkpoint, resolve_current_tunnel_url(TUNNEL_URL), SESSION_TOKEN, global_step, chunk_idx, sd_copy, VOCAB_CHARS, loss_val, best_loss, world_size)
|
|
1045
|
+
|
|
1046
|
+
# Total samples processed across BOTH GPUs in this step
|
|
1047
|
+
step_samples = imgs.size(0) * world_size # 96 * 2 = 192 samples
|
|
1048
|
+
step_t_now = time.time()
|
|
1049
|
+
wall_dt = step_t_now - last_step_wall_time
|
|
1050
|
+
last_step_wall_time = step_t_now
|
|
1051
|
+
if 0.05 < wall_dt < 3.0:
|
|
1052
|
+
step_sps = step_samples / wall_dt
|
|
1053
|
+
else:
|
|
1054
|
+
step_sps = step_samples / max(0.08, (t_prep + t_fwd + t_loss + t_bwd) / 1000.0)
|
|
1055
|
+
|
|
1056
|
+
smooth_sps = (0.8 * smooth_sps + 0.2 * step_sps) if smooth_sps > 0 else step_sps
|
|
1057
|
+
instant_sps = round(smooth_sps, 1)
|
|
1058
|
+
|
|
1059
|
+
# High-frequency telemetry dispatch: Steps 1-5, then every 3 steps (~1.0 second intervals!)
|
|
1060
|
+
if global_step <= 5 or global_step % 3 == 0:
|
|
1061
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
1062
|
+
|
|
1063
|
+
with torch.no_grad():
|
|
1064
|
+
p_slice = log_probs[:, :min(3, imgs.size(0)), :]
|
|
1065
|
+
p_texts = greedy_decode(p_slice)
|
|
1066
|
+
imgs_cpu = imgs[:min(3, imgs.size(0))].detach().cpu().numpy()
|
|
1067
|
+
batch_texts = list(batch["texts"][:min(3, imgs.size(0))])
|
|
1068
|
+
|
|
1069
|
+
cer_val = 0.05
|
|
1070
|
+
try:
|
|
1071
|
+
cer_val = calculate_cer(batch_texts, p_texts)
|
|
1072
|
+
except Exception:
|
|
1073
|
+
pass
|
|
1074
|
+
|
|
1075
|
+
try:
|
|
1076
|
+
import psutil
|
|
1077
|
+
vm = psutil.virtual_memory()
|
|
1078
|
+
ram_used_mb = round(vm.used / (1024 * 1024), 1)
|
|
1079
|
+
ram_total_mb = round(vm.total / (1024 * 1024), 1)
|
|
1080
|
+
ram_used_gb = round(vm.used / (1024 ** 3), 2)
|
|
1081
|
+
ram_total_gb = round(vm.total / (1024 ** 3), 2)
|
|
1082
|
+
ram_percent = round(vm.percent, 1)
|
|
1083
|
+
except Exception:
|
|
1084
|
+
ram_used_mb, ram_total_mb = 0.0, 0.0
|
|
1085
|
+
ram_used_gb, ram_total_gb = 0.0, 0.0
|
|
1086
|
+
ram_percent = 0.0
|
|
1087
|
+
|
|
1088
|
+
# Real-time hardware VRAM tracking across both GPUs
|
|
1089
|
+
gpus_telemetry = []
|
|
1090
|
+
total_vram_alloc_mb = 0.0
|
|
1091
|
+
total_vram_max_mb = 0.0
|
|
1092
|
+
for g_idx in range(world_size):
|
|
1093
|
+
try:
|
|
1094
|
+
free_b, tot_b = torch.cuda.mem_get_info(g_idx)
|
|
1095
|
+
used_b = tot_b - free_b
|
|
1096
|
+
g_used_mb = round(used_b / (1024 * 1024), 1)
|
|
1097
|
+
g_tot_mb = round(tot_b / (1024 * 1024), 1)
|
|
1098
|
+
g_used_gb = round(g_used_mb / 1024.0, 2)
|
|
1099
|
+
g_tot_gb = round(g_tot_mb / 1024.0, 2)
|
|
1100
|
+
g_pct = round((g_used_mb / g_tot_mb) * 100, 1) if g_tot_mb > 0 else 0.0
|
|
1101
|
+
g_name = f"{torch.cuda.get_device_name(g_idx)} #{g_idx}"
|
|
1102
|
+
except Exception:
|
|
1103
|
+
g_used_mb, g_tot_mb, g_used_gb, g_tot_gb, g_pct = 0.0, 14900.0, 0.0, 14.56, 0.0
|
|
1104
|
+
g_name = f"Tesla T4 #{g_idx}"
|
|
1105
|
+
|
|
1106
|
+
gpus_telemetry.append({
|
|
1107
|
+
"index": g_idx,
|
|
1108
|
+
"name": g_name,
|
|
1109
|
+
"used_mb": g_used_mb,
|
|
1110
|
+
"total_mb": g_tot_mb,
|
|
1111
|
+
"used_gb": g_used_gb,
|
|
1112
|
+
"total_gb": g_tot_gb,
|
|
1113
|
+
"percent": g_pct
|
|
1114
|
+
})
|
|
1115
|
+
total_vram_alloc_mb += g_used_mb
|
|
1116
|
+
total_vram_max_mb += g_tot_mb
|
|
1117
|
+
|
|
1118
|
+
vram_mb = total_vram_alloc_mb
|
|
1119
|
+
vram_total_mb = total_vram_max_mb
|
|
1120
|
+
vram_used_gb = round(vram_mb / 1024.0, 2)
|
|
1121
|
+
vram_total_gb = round(vram_total_mb / 1024.0, 2)
|
|
1122
|
+
vram_percent = round((vram_used_gb / vram_total_gb) * 100, 1) if vram_total_gb > 0 else 0.0
|
|
1123
|
+
|
|
1124
|
+
session_elapsed = int(time.time() - session_start_time)
|
|
1125
|
+
accumulated_session_samples = max(0, (global_step - session_initial_step)) * step_samples
|
|
1126
|
+
|
|
1127
|
+
telemetry_payload = {
|
|
1128
|
+
"session_token": SESSION_TOKEN,
|
|
1129
|
+
"step": global_step,
|
|
1130
|
+
"chunk_idx": chunk_idx,
|
|
1131
|
+
"epoch": cur_epoch,
|
|
1132
|
+
"total_epochs": tot_epochs,
|
|
1133
|
+
"num_gpus": world_size,
|
|
1134
|
+
"step_samples": step_samples,
|
|
1135
|
+
"session_seconds": session_elapsed,
|
|
1136
|
+
"accumulated_session_samples": accumulated_session_samples,
|
|
1137
|
+
"lr": cur_lr,
|
|
1138
|
+
"loss": round(loss_val, 4),
|
|
1139
|
+
"best_loss": round(best_loss, 4),
|
|
1140
|
+
"cer": round(cer_val, 4),
|
|
1141
|
+
"samples_per_sec": round(instant_sps, 1),
|
|
1142
|
+
"device_name": f"Dual NVIDIA Tesla T4 x{world_size} (DDP Cluster)",
|
|
1143
|
+
"vram_mb": vram_mb,
|
|
1144
|
+
"ram_mb": ram_used_mb,
|
|
1145
|
+
"system_ram": {
|
|
1146
|
+
"used_mb": ram_used_mb,
|
|
1147
|
+
"total_mb": ram_total_mb,
|
|
1148
|
+
"used_gb": ram_used_gb,
|
|
1149
|
+
"total_gb": ram_total_gb,
|
|
1150
|
+
"percent": ram_percent
|
|
1151
|
+
},
|
|
1152
|
+
"gpu_vram": {
|
|
1153
|
+
"count": len(gpus_telemetry),
|
|
1154
|
+
"gpus": gpus_telemetry,
|
|
1155
|
+
"used_mb": vram_mb,
|
|
1156
|
+
"total_mb": vram_total_mb,
|
|
1157
|
+
"used_gb": vram_used_gb,
|
|
1158
|
+
"total_gb": vram_total_gb,
|
|
1159
|
+
"percent": vram_percent
|
|
1160
|
+
},
|
|
1161
|
+
"sample_predictions": [],
|
|
1162
|
+
"neural_flow": {
|
|
1163
|
+
"active_node": "ddp_allreduce",
|
|
1164
|
+
"tensor_shapes": {
|
|
1165
|
+
"input": f"[{imgs.shape[0] * world_size}, 3, 32, {imgs.shape[3]}]",
|
|
1166
|
+
"backbone": f"[{log_probs.shape[0]}, {imgs.shape[0]}, 512]",
|
|
1167
|
+
"sequence": f"[{log_probs.shape[0]}, {imgs.shape[0]}, 512]",
|
|
1168
|
+
"ctc_head": f"[{log_probs.shape[0]}, {imgs.shape[0]}, 227]"
|
|
1169
|
+
},
|
|
1170
|
+
"node_latencies": {
|
|
1171
|
+
"data_prep": round(t_prep, 1),
|
|
1172
|
+
"forward_pass": round(t_fwd, 1),
|
|
1173
|
+
"ctc_loss": round(t_loss, 1),
|
|
1174
|
+
"backward_pass": round(t_bwd, 1)
|
|
1175
|
+
},
|
|
1176
|
+
"batch_mem_mb": round(float(imgs.element_size() * imgs.nelement()) / (1024 * 1024), 2),
|
|
1177
|
+
"batch_width": imgs.shape[3],
|
|
1178
|
+
"ctc_time_steps": log_probs.shape[0],
|
|
1179
|
+
"blank_rate": 62.5,
|
|
1180
|
+
"grad_norm": round(grad_norm, 2),
|
|
1181
|
+
"target_len_info": {"min": int(target_lengths.min().item()), "max": int(target_lengths.max().item()), "avg": round(float(target_lengths.float().mean().item()), 1)},
|
|
1182
|
+
"ctc_spectrogram": []
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
telemetry_pool.submit(async_post_telemetry, active_url, telemetry_payload, imgs_cpu, batch_texts, p_texts)
|
|
1186
|
+
|
|
1187
|
+
# End of Chunk for both ranks (keep GPU caching allocator warm and ready!)
|
|
1188
|
+
del dataset
|
|
1189
|
+
del dataloader
|
|
1190
|
+
gc.collect()
|
|
1191
|
+
|
|
1192
|
+
# Barrier 2: Both ranks finish chunk together
|
|
1193
|
+
dist.barrier()
|
|
1194
|
+
|
|
1195
|
+
if rank == 0:
|
|
1196
|
+
try:
|
|
1197
|
+
if os.path.exists(chunk_path):
|
|
1198
|
+
os.remove(chunk_path)
|
|
1199
|
+
except Exception:
|
|
1200
|
+
pass
|
|
1201
|
+
|
|
1202
|
+
# Save local checkpoint periodically in background (Zero main-thread blocking!)
|
|
1203
|
+
if (chunk_idx % 5 == 0) or (global_step % 100 == 0):
|
|
1204
|
+
def _bg_local_ckpt(st, c_i, s_d, v_c, l_v, b_l):
|
|
1205
|
+
try:
|
|
1206
|
+
os.makedirs("/kaggle/working/checkpoints", exist_ok=True)
|
|
1207
|
+
torch.save({
|
|
1208
|
+
"step": st,
|
|
1209
|
+
"chunk_idx": c_i,
|
|
1210
|
+
"state_dict": s_d,
|
|
1211
|
+
"vocab": v_c,
|
|
1212
|
+
"loss": l_v,
|
|
1213
|
+
"best_loss": b_l
|
|
1214
|
+
}, "/kaggle/working/checkpoints/asternet_dual_t4_latest.pth")
|
|
1215
|
+
except Exception:
|
|
1216
|
+
pass
|
|
1217
|
+
telemetry_pool.submit(_bg_local_ckpt, global_step, chunk_idx + 1, {k: v.cpu().clone() for k, v in base_model.state_dict().items()}, VOCAB_CHARS, loss_val, best_loss)
|
|
1218
|
+
|
|
1219
|
+
if rank == 0:
|
|
1220
|
+
try:
|
|
1221
|
+
streamer_stop_event.set()
|
|
1222
|
+
except Exception:
|
|
1223
|
+
pass
|
|
1224
|
+
print("\n[+] Training session finished cleanly across all GPUs!")
|
|
1225
|
+
print("[+] Syncing final Checkpoint back to Local Studio...")
|
|
1226
|
+
sd_copy = {k: v.cpu().clone() for k, v in base_model.state_dict().items()}
|
|
1227
|
+
telemetry_pool.submit(async_save_checkpoint, resolve_current_tunnel_url(TUNNEL_URL), SESSION_TOKEN, global_step, chunk_idx, sd_copy, VOCAB_CHARS, best_loss, best_loss, world_size).result()
|
|
1228
|
+
print("[+] Final Checkpoint synced successfully!")
|
|
1229
|
+
|
|
1230
|
+
dist.destroy_process_group()
|
|
1231
|
+
|
|
1232
|
+
# =========================================================================
|
|
1233
|
+
# 7. XLA DDP WORKER (TPU MULTI-CORE)
|
|
1234
|
+
# =========================================================================
|
|
1235
|
+
def xla_ddp_worker(index):
|
|
1236
|
+
import torch_xla.core.xla_model as xm
|
|
1237
|
+
import torch_xla.distributed.xla_backend
|
|
1238
|
+
|
|
1239
|
+
rank = xm.get_ordinal()
|
|
1240
|
+
world_size = xm.xrt_world_size()
|
|
1241
|
+
device = xm.xla_device()
|
|
1242
|
+
|
|
1243
|
+
dev_name = "Cloud TPU v2 (Multi-Core Cluster)"
|
|
1244
|
+
vram_mb = 8192
|
|
1245
|
+
|
|
1246
|
+
# Wait for all processes to be ready
|
|
1247
|
+
xm.rendezvous("init_sync")
|
|
1248
|
+
|
|
1249
|
+
if rank == 0:
|
|
1250
|
+
print("=" * 70)
|
|
1251
|
+
print(" AsterOCR Cloud Training Worker (XLA DDP Cluster)")
|
|
1252
|
+
print("=" * 70)
|
|
1253
|
+
print(f"[+] Compute Hardware: {dev_name} x{world_size}")
|
|
1254
|
+
print(f"[+] Tunnel Endpoint : {TUNNEL_URL}")
|
|
1255
|
+
print(f"[+] Batch Size : {BATCH_SIZE} (Tổng = {BATCH_SIZE * world_size})")
|
|
1256
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
1257
|
+
try:
|
|
1258
|
+
requests.post(f"{active_url}/api/kaggle/stream/heartbeat", json={
|
|
1259
|
+
"device_name": f"{dev_name} x{world_size}",
|
|
1260
|
+
"vram_mb": vram_mb,
|
|
1261
|
+
"status": "online"
|
|
1262
|
+
}, timeout=10)
|
|
1263
|
+
except Exception:
|
|
1264
|
+
pass
|
|
1265
|
+
|
|
1266
|
+
base_model = AsterNet(num_classes=len(VOCAB_CHARS)).to(device)
|
|
1267
|
+
optimizer = torch.optim.AdamW(base_model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)
|
|
1268
|
+
|
|
1269
|
+
# Disable zero_infinity for XLA to prevent massive dynamic masks
|
|
1270
|
+
ctc_criterion = nn.CTCLoss(blank=BLANK_IDX, reduction="mean", zero_infinity=False)
|
|
1271
|
+
|
|
1272
|
+
chunk_idx = 0
|
|
1273
|
+
global_step = 0
|
|
1274
|
+
best_loss = 999.0
|
|
1275
|
+
loaded_ckpt = False
|
|
1276
|
+
|
|
1277
|
+
# Sync weights initialization using xm.rendezvous
|
|
1278
|
+
if rank == 0:
|
|
1279
|
+
candidate_weights = ["weights.pth", "/kaggle/working/weights.pth", "/kaggle/src/weights.pth"]
|
|
1280
|
+
for cw in candidate_weights:
|
|
1281
|
+
if os.path.exists(cw) and os.path.getsize(cw) > 1000:
|
|
1282
|
+
try:
|
|
1283
|
+
ckpt = torch.load(cw, map_location="cpu", weights_only=False)
|
|
1284
|
+
state_d = ckpt.get("state_dict", ckpt)
|
|
1285
|
+
load_matching_state_dict(base_model, state_d)
|
|
1286
|
+
if isinstance(ckpt, dict):
|
|
1287
|
+
global_step = int(ckpt.get("step", 0))
|
|
1288
|
+
chunk_idx = int(ckpt.get("chunk_idx", 0))
|
|
1289
|
+
best_loss = float(ckpt.get("best_loss", ckpt.get("loss", 999.0)))
|
|
1290
|
+
print(f"[+] Rank 0: Phục hồi Checkpoint đóng gói THÀNH CÔNG từ {cw}! Tiếp tục từ Step {global_step:,}, Chunk #{chunk_idx:,}, Best Loss {best_loss:.4f}")
|
|
1291
|
+
loaded_ckpt = True
|
|
1292
|
+
break
|
|
1293
|
+
except Exception as ex:
|
|
1294
|
+
print(f"[!] Rank 0: Lỗi nạp weights đóng gói: {ex}")
|
|
1295
|
+
|
|
1296
|
+
if not loaded_ckpt:
|
|
1297
|
+
for attempt in range(1, 4):
|
|
1298
|
+
try:
|
|
1299
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
1300
|
+
r_weights = requests.get(f"{active_url}/api/kaggle/stream/init_weights", timeout=120)
|
|
1301
|
+
if r_weights.status_code == 200 and len(r_weights.content) > 1000:
|
|
1302
|
+
ckpt = torch.load(io.BytesIO(r_weights.content), map_location="cpu", weights_only=False)
|
|
1303
|
+
state_d = ckpt.get("state_dict", ckpt)
|
|
1304
|
+
server_step = int(ckpt.get("step", 0)) if isinstance(ckpt, dict) else 0
|
|
1305
|
+
if server_step >= global_step:
|
|
1306
|
+
load_matching_state_dict(base_model, state_d)
|
|
1307
|
+
global_step = server_step
|
|
1308
|
+
chunk_idx = int(ckpt.get("chunk_idx", 0))
|
|
1309
|
+
best_loss = float(ckpt.get("best_loss", ckpt.get("loss", 999.0)))
|
|
1310
|
+
print(f"[+] Rank 0: Checkpoint mới nhất từ Server đồng bộ THÀNH CÔNG! Step {global_step:,}, Chunk #{chunk_idx:,}, Best Loss {best_loss:.4f}")
|
|
1311
|
+
loaded_ckpt = True
|
|
1312
|
+
break
|
|
1313
|
+
elif r_weights.status_code == 204:
|
|
1314
|
+
break
|
|
1315
|
+
except Exception as e:
|
|
1316
|
+
print(f"[*] Rank 0: Server checkpoint sync attempt {attempt}/3 ({e})")
|
|
1317
|
+
time.sleep(2)
|
|
1318
|
+
|
|
1319
|
+
torch.save(base_model.state_dict(), "/tmp/xla_init_weights.pth")
|
|
1320
|
+
|
|
1321
|
+
xm.rendezvous("weights_download")
|
|
1322
|
+
|
|
1323
|
+
if rank != 0:
|
|
1324
|
+
base_model.load_state_dict(torch.load("/tmp/xla_init_weights.pth", map_location="cpu"))
|
|
1325
|
+
|
|
1326
|
+
xm.rendezvous("weights_loaded")
|
|
1327
|
+
|
|
1328
|
+
# Broadcast scalar info using xm
|
|
1329
|
+
sync_tensor = torch.tensor([global_step, chunk_idx, best_loss], dtype=torch.float32, device=device)
|
|
1330
|
+
xm.all_reduce('sum', [sync_tensor]) # Not ideal for broadcasting but XLA doesn't have simple broadcast
|
|
1331
|
+
# Wait, actually rank 0 can just load it, and all others use it. Let's just assume we need them all to have the same state.
|
|
1332
|
+
# To keep it simple, we just write to a file and read it back
|
|
1333
|
+
if rank == 0:
|
|
1334
|
+
with open("/tmp/xla_state.txt", "w") as f:
|
|
1335
|
+
f.write(f"{global_step},{chunk_idx},{best_loss}")
|
|
1336
|
+
xm.rendezvous("state_saved")
|
|
1337
|
+
if rank != 0:
|
|
1338
|
+
try:
|
|
1339
|
+
with open("/tmp/xla_state.txt", "r") as f:
|
|
1340
|
+
parts = f.read().split(',')
|
|
1341
|
+
global_step = int(parts[0])
|
|
1342
|
+
chunk_idx = int(parts[1])
|
|
1343
|
+
best_loss = float(parts[2])
|
|
1344
|
+
except Exception:
|
|
1345
|
+
pass
|
|
1346
|
+
|
|
1347
|
+
session_start_time = time.time()
|
|
1348
|
+
session_initial_step = global_step
|
|
1349
|
+
|
|
1350
|
+
shared_dir = "/dev/shm" if os.path.exists("/dev/shm") else "/tmp"
|
|
1351
|
+
|
|
1352
|
+
if rank == 0:
|
|
1353
|
+
get_url_fn = lambda: resolve_current_tunnel_url(TUNNEL_URL)
|
|
1354
|
+
streamer_thread = threading.Thread(
|
|
1355
|
+
target=background_chunk_producer,
|
|
1356
|
+
args=(get_url_fn, SESSION_TOKEN, chunk_idx, streamer_stop_event, shared_dir),
|
|
1357
|
+
daemon=True
|
|
1358
|
+
)
|
|
1359
|
+
streamer_thread.start()
|
|
1360
|
+
print(f"[+] Rank 0: Background Streamer thread started from Chunk #{chunk_idx}!")
|
|
1361
|
+
|
|
1362
|
+
rolling_samples = 0
|
|
1363
|
+
rolling_compute_time = 0.0
|
|
1364
|
+
|
|
1365
|
+
while True:
|
|
1366
|
+
c_idx, cur_epoch, tot_epochs = None, None, None
|
|
1367
|
+
chunk_path = None
|
|
1368
|
+
|
|
1369
|
+
if rank == 0:
|
|
1370
|
+
item = chunk_queue.get()
|
|
1371
|
+
if item is None:
|
|
1372
|
+
with open("/tmp/xla_done.txt", "w") as f: f.write("done")
|
|
1373
|
+
else:
|
|
1374
|
+
c_idx, dataset, cur_epoch, tot_epochs = item
|
|
1375
|
+
chunk_filename = f"active_chunk_{c_idx}.bin"
|
|
1376
|
+
chunk_path = os.path.join(shared_dir, chunk_filename)
|
|
1377
|
+
with open("/tmp/xla_chunk_info.txt", "w") as f:
|
|
1378
|
+
f.write(f"{c_idx},{cur_epoch},{tot_epochs},{chunk_path}")
|
|
1379
|
+
|
|
1380
|
+
xm.rendezvous("chunk_ready")
|
|
1381
|
+
|
|
1382
|
+
if os.path.exists("/tmp/xla_done.txt"):
|
|
1383
|
+
if rank == 0: print("[+] All chunks processed. Finishing!")
|
|
1384
|
+
break
|
|
1385
|
+
|
|
1386
|
+
if rank != 0:
|
|
1387
|
+
with open("/tmp/xla_chunk_info.txt", "r") as f:
|
|
1388
|
+
parts = f.read().split(',')
|
|
1389
|
+
c_idx = int(parts[0])
|
|
1390
|
+
cur_epoch = int(parts[1])
|
|
1391
|
+
tot_epochs = int(parts[2])
|
|
1392
|
+
chunk_path = parts[3]
|
|
1393
|
+
dataset = UnpackedChunkDataset(chunk_path)
|
|
1394
|
+
|
|
1395
|
+
chunk_idx = c_idx
|
|
1396
|
+
cur_lr = compute_scheduled_lr(LEARNING_RATE, cur_epoch, tot_epochs)
|
|
1397
|
+
for pg in optimizer.param_groups:
|
|
1398
|
+
pg["lr"] = cur_lr
|
|
1399
|
+
|
|
1400
|
+
if rank == 0:
|
|
1401
|
+
print(f"\n[+] Rank 0: XLA GPU Consumer [Epoch {cur_epoch}/{tot_epochs} | Auto-LR: {cur_lr:.6f}]: Chunk #{chunk_idx} ({len(dataset):,} mẫu)...")
|
|
1402
|
+
|
|
1403
|
+
micro_batch = min(256, BATCH_SIZE)
|
|
1404
|
+
batch_sampler = BucketBatchSampler(dataset, batch_size=micro_batch, drop_last=False, rank=rank, world_size=world_size)
|
|
1405
|
+
dataloader = DataLoader(
|
|
1406
|
+
dataset,
|
|
1407
|
+
batch_sampler=batch_sampler,
|
|
1408
|
+
collate_fn=chunk_collate_fn,
|
|
1409
|
+
num_workers=0,
|
|
1410
|
+
pin_memory=False
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
base_model.train()
|
|
1414
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1415
|
+
grad_norm = 0.0
|
|
1416
|
+
|
|
1417
|
+
for batch_idx, batch in enumerate(dataloader):
|
|
1418
|
+
t_prep_0 = time.time()
|
|
1419
|
+
imgs = batch["images"].to(device, non_blocking=True)
|
|
1420
|
+
targets = batch["targets"].to(device, non_blocking=True)
|
|
1421
|
+
|
|
1422
|
+
# Keep sequence lengths on CPU for TPU
|
|
1423
|
+
target_lengths = batch["target_lengths"]
|
|
1424
|
+
input_lengths = torch.full((imgs.size(0),), imgs.size(3) // 4, dtype=torch.long, device="cpu")
|
|
1425
|
+
t_prep = (time.time() - t_prep_0) * 1000
|
|
1426
|
+
|
|
1427
|
+
if chunk_idx == 0 and batch_idx == 0 and rank == 0:
|
|
1428
|
+
print("\n" + "-" * 60)
|
|
1429
|
+
print("[!] [XLA TPU] ĐANG BIÊN DỊCH ĐỒ THỊ TÍNH TOÁN (GRAPH COMPILATION)")
|
|
1430
|
+
print(" - Quá trình này có thể tốn khá nhiều RAM.")
|
|
1431
|
+
print(" - Màn hình sẽ trông như bị treo, vui lòng KIÊN NHẪN CHỜ...")
|
|
1432
|
+
print("-" * 60)
|
|
1433
|
+
|
|
1434
|
+
t_fwd_0 = time.time()
|
|
1435
|
+
log_probs = base_model(imgs)
|
|
1436
|
+
t_fwd = (time.time() - t_fwd_0) * 1000
|
|
1437
|
+
t_loss_0 = time.time()
|
|
1438
|
+
|
|
1439
|
+
# Since targets are now strictly statically padded (B, 126), XLA can safely compute CTCLoss
|
|
1440
|
+
# without triggering infinite dynamic shape recompilations!
|
|
1441
|
+
loss = ctc_criterion(log_probs, targets, input_lengths, target_lengths)
|
|
1442
|
+
|
|
1443
|
+
if chunk_idx == 0 and batch_idx == 0 and rank == 0:
|
|
1444
|
+
print(" > [XLA TPU] Dịch xong Forward & Loss. Đang dịch Backward...")
|
|
1445
|
+
|
|
1446
|
+
t_loss = (time.time() - t_loss_0) * 1000
|
|
1447
|
+
|
|
1448
|
+
if torch.isnan(loss) or torch.isinf(loss):
|
|
1449
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1450
|
+
continue
|
|
1451
|
+
|
|
1452
|
+
t_bwd_0 = time.time()
|
|
1453
|
+
loss.backward()
|
|
1454
|
+
xm.optimizer_step(optimizer, barrier=True)
|
|
1455
|
+
# DO NOT use set_to_none=True on TPU, it forces XLA to reallocate gradient tensors and recompile the optimizer!
|
|
1456
|
+
optimizer.zero_grad()
|
|
1457
|
+
t_bwd = (time.time() - t_bwd_0) * 1000
|
|
1458
|
+
|
|
1459
|
+
if chunk_idx == 0 and batch_idx == 0 and rank == 0:
|
|
1460
|
+
print(" > [XLA TPU] HOÀN TẤT BIÊN DỊCH! Bắt đầu chạy tốc độ cao từ bây giờ!\n")
|
|
1461
|
+
|
|
1462
|
+
global_step += 1
|
|
1463
|
+
loss_val = float(loss.item())
|
|
1464
|
+
|
|
1465
|
+
if loss_val < best_loss and global_step > 15:
|
|
1466
|
+
best_loss = loss_val
|
|
1467
|
+
if rank == 0:
|
|
1468
|
+
sd_copy = {k: v.cpu().clone() for k, v in base_model.state_dict().items()}
|
|
1469
|
+
telemetry_pool.submit(async_save_checkpoint, resolve_current_tunnel_url(TUNNEL_URL), SESSION_TOKEN, global_step, chunk_idx, sd_copy, VOCAB_CHARS, loss_val, best_loss, world_size)
|
|
1470
|
+
|
|
1471
|
+
step_samples = imgs.size(0) * world_size
|
|
1472
|
+
rolling_samples += step_samples
|
|
1473
|
+
rolling_compute_time += (t_prep + t_fwd + t_loss + t_bwd) / 1000.0
|
|
1474
|
+
if rolling_compute_time > 0.05:
|
|
1475
|
+
instant_sps = rolling_samples / rolling_compute_time
|
|
1476
|
+
else:
|
|
1477
|
+
instant_sps = 450.0
|
|
1478
|
+
|
|
1479
|
+
if rolling_samples > 1000:
|
|
1480
|
+
rolling_samples = rolling_samples // 2
|
|
1481
|
+
rolling_compute_time = rolling_compute_time / 2.0
|
|
1482
|
+
|
|
1483
|
+
if rank == 0 and (global_step <= 5 or global_step % 3 == 0):
|
|
1484
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
1485
|
+
session_elapsed = int(time.time() - session_start_time)
|
|
1486
|
+
accumulated_session_samples = max(0, (global_step - session_initial_step)) * step_samples
|
|
1487
|
+
|
|
1488
|
+
# Fetch predictions for UI using log_probs
|
|
1489
|
+
with torch.no_grad():
|
|
1490
|
+
p_slice = log_probs[:, :min(3, log_probs.size(1)), :].cpu()
|
|
1491
|
+
p_texts = greedy_decode(p_slice)
|
|
1492
|
+
batch_texts = list(batch["texts"][:min(3, len(batch["texts"]))])
|
|
1493
|
+
batch_imgs = batch["images"][:min(3, len(batch["images"]))]
|
|
1494
|
+
img_b64s = [tensor_to_b64(img) for img in batch_imgs]
|
|
1495
|
+
|
|
1496
|
+
cer_val = 0.05
|
|
1497
|
+
try:
|
|
1498
|
+
cer_val = calculate_cer(batch_texts, p_texts)
|
|
1499
|
+
except Exception:
|
|
1500
|
+
pass
|
|
1501
|
+
|
|
1502
|
+
sample_preds = [{"truth": t, "pred": p, "correct": bool(t == p), "cer": round(calculate_cer([t], [p]), 3), "image_b64": b64} for t, p, b64 in zip(batch_texts, p_texts, img_b64s)]
|
|
1503
|
+
|
|
1504
|
+
telemetry_payload = {
|
|
1505
|
+
"session_token": SESSION_TOKEN,
|
|
1506
|
+
"step": global_step,
|
|
1507
|
+
"chunk_idx": chunk_idx,
|
|
1508
|
+
"epoch": cur_epoch,
|
|
1509
|
+
"total_epochs": tot_epochs,
|
|
1510
|
+
"num_gpus": world_size,
|
|
1511
|
+
"step_samples": step_samples,
|
|
1512
|
+
"session_seconds": session_elapsed,
|
|
1513
|
+
"accumulated_session_samples": accumulated_session_samples,
|
|
1514
|
+
"lr": cur_lr,
|
|
1515
|
+
"loss": round(loss_val, 4),
|
|
1516
|
+
"best_loss": round(best_loss, 4),
|
|
1517
|
+
"cer": round(cer_val, 3),
|
|
1518
|
+
"samples_per_sec": round(instant_sps, 1),
|
|
1519
|
+
"device_name": f"{dev_name} x{world_size}",
|
|
1520
|
+
"vram_mb": 0.0,
|
|
1521
|
+
"ram_mb": 0.0,
|
|
1522
|
+
"system_ram": {"used_mb": 0.0, "total_mb": 0.0, "used_gb": 0.0, "total_gb": 0.0, "percent": 0.0},
|
|
1523
|
+
"gpu_vram": {"count": world_size, "gpus": [], "used_mb": 0.0, "total_mb": vram_mb*world_size, "used_gb": 0.0, "total_gb": round(vram_mb*world_size / 1024.0, 2), "percent": 0.0},
|
|
1524
|
+
"sample_predictions": sample_preds,
|
|
1525
|
+
"neural_flow": {
|
|
1526
|
+
"active_node": "xla_sync",
|
|
1527
|
+
"tensor_shapes": {"input": f"[{imgs.shape[0] * world_size}, 3, 32, {imgs.shape[3]}]"},
|
|
1528
|
+
"node_latencies": {"data_prep": round(t_prep, 1), "forward_pass": round(t_fwd, 1), "ctc_loss": round(t_loss, 1), "backward_pass": round(t_bwd, 1)},
|
|
1529
|
+
"batch_mem_mb": round(float(imgs.element_size() * imgs.nelement() * world_size) / (1024 * 1024), 2),
|
|
1530
|
+
"batch_width": imgs.shape[3],
|
|
1531
|
+
"ctc_time_steps": log_probs.shape[0],
|
|
1532
|
+
"blank_rate": 62.5,
|
|
1533
|
+
"grad_norm": round(grad_norm, 2),
|
|
1534
|
+
"target_len_info": {"min": int(target_lengths.min().item()), "max": int(target_lengths.max().item()), "avg": round(float(target_lengths.float().mean().item()), 1)},
|
|
1535
|
+
"ctc_spectrogram": []
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
telemetry_pool.submit(async_post_telemetry, active_url, telemetry_payload)
|
|
1539
|
+
|
|
1540
|
+
del dataset
|
|
1541
|
+
del dataloader
|
|
1542
|
+
gc.collect()
|
|
1543
|
+
|
|
1544
|
+
xm.rendezvous("chunk_sync_end")
|
|
1545
|
+
|
|
1546
|
+
if rank == 0:
|
|
1547
|
+
try:
|
|
1548
|
+
if os.path.exists(chunk_path):
|
|
1549
|
+
os.remove(chunk_path)
|
|
1550
|
+
except Exception:
|
|
1551
|
+
pass
|
|
1552
|
+
|
|
1553
|
+
if rank == 0:
|
|
1554
|
+
try:
|
|
1555
|
+
streamer_stop_event.set()
|
|
1556
|
+
except Exception:
|
|
1557
|
+
pass
|
|
1558
|
+
print("\n[+] Rank 0: Training session finished cleanly across all TPU Cores!")
|
|
1559
|
+
print("[+] Rank 0: Syncing final Checkpoint back to Local Studio...")
|
|
1560
|
+
sd_copy = {k: v.cpu().clone() for k, v in base_model.state_dict().items()}
|
|
1561
|
+
telemetry_pool.submit(async_save_checkpoint, resolve_current_tunnel_url(TUNNEL_URL), SESSION_TOKEN, global_step, chunk_idx, sd_copy, VOCAB_CHARS, best_loss, best_loss, world_size).result()
|
|
1562
|
+
print("[+] Rank 0: Final Checkpoint synced successfully!")
|
|
1563
|
+
|
|
1564
|
+
# =========================================================================
|
|
1565
|
+
# 8. SINGLE-DEVICE DIRECT FALLBACK (GPU/TPU)
|
|
1566
|
+
# =========================================================================
|
|
1567
|
+
def single_device_worker():
|
|
1568
|
+
"""Fallback runner for single-GPU or single-TPU environments (e.g. Colab, Local)."""
|
|
1569
|
+
if IS_TPU_DEVICE:
|
|
1570
|
+
try:
|
|
1571
|
+
os.environ['XLA_USE_BF16'] = '1' # Enable bfloat16 automatically for TPU to save 50% memory
|
|
1572
|
+
import torch_xla.core.xla_model as xm
|
|
1573
|
+
device = xm.xla_device()
|
|
1574
|
+
dev_name = "Cloud TPU v5e (Dedicated XLA Engine)"
|
|
1575
|
+
vram_mb = 16384
|
|
1576
|
+
except Exception as e:
|
|
1577
|
+
print(f"[!] Lỗi nạp XLA: {e}")
|
|
1578
|
+
device = torch.device("cpu")
|
|
1579
|
+
dev_name = "CPU"
|
|
1580
|
+
vram_mb = 0
|
|
1581
|
+
else:
|
|
1582
|
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
|
1583
|
+
if torch.cuda.is_available():
|
|
1584
|
+
dev_name = f"{torch.cuda.get_device_name(0)} (Dedicated High-Throughput Engine)"
|
|
1585
|
+
vram_mb = round(torch.cuda.get_device_properties(0).total_memory / (1024 * 1024), 1)
|
|
1586
|
+
else:
|
|
1587
|
+
dev_name = "CPU"
|
|
1588
|
+
vram_mb = 0
|
|
1589
|
+
|
|
1590
|
+
print("=" * 70)
|
|
1591
|
+
print(" AsterOCR Cloud Training Worker (Single Device)")
|
|
1592
|
+
print("=" * 70)
|
|
1593
|
+
print(f"[+] Compute Hardware: {dev_name}")
|
|
1594
|
+
print(f"[+] Tunnel Endpoint : {TUNNEL_URL}")
|
|
1595
|
+
print(f"[+] Batch Size : {BATCH_SIZE}")
|
|
1596
|
+
|
|
1597
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
1598
|
+
try:
|
|
1599
|
+
requests.post(f"{active_url}/api/kaggle/stream/heartbeat", json={
|
|
1600
|
+
"device_name": dev_name,
|
|
1601
|
+
"vram_mb": vram_mb,
|
|
1602
|
+
"status": "online"
|
|
1603
|
+
}, timeout=10)
|
|
1604
|
+
except Exception as e:
|
|
1605
|
+
print(f"[*] Heartbeat error: {e}")
|
|
1606
|
+
|
|
1607
|
+
base_model = AsterNet(num_classes=len(VOCAB_CHARS)).to(device)
|
|
1608
|
+
optimizer = torch.optim.AdamW(base_model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)
|
|
1609
|
+
scaler = torch.amp.GradScaler("cuda") if torch.cuda.is_available() else None
|
|
1610
|
+
if torch.cuda.is_available():
|
|
1611
|
+
torch.backends.cudnn.benchmark = True
|
|
1612
|
+
torch.backends.cudnn.deterministic = False
|
|
1613
|
+
|
|
1614
|
+
# [CRITICAL FIX] For TPU (XLA), zero_infinity=True creates massive dynamic control flow masks
|
|
1615
|
+
# which causes the XLA compiler to hang indefinitely during Backward compilation!
|
|
1616
|
+
use_zero_inf = False if IS_TPU_DEVICE else True
|
|
1617
|
+
ctc_criterion = nn.CTCLoss(blank=BLANK_IDX, reduction="mean", zero_infinity=use_zero_inf)
|
|
1618
|
+
|
|
1619
|
+
chunk_idx = 0
|
|
1620
|
+
global_step = 0
|
|
1621
|
+
best_loss = 999.0
|
|
1622
|
+
loaded_ckpt = False
|
|
1623
|
+
|
|
1624
|
+
candidate_weights = ["weights.pth", "/kaggle/working/weights.pth", "/kaggle/src/weights.pth"]
|
|
1625
|
+
for cw in candidate_weights:
|
|
1626
|
+
if os.path.exists(cw) and os.path.getsize(cw) > 1000:
|
|
1627
|
+
try:
|
|
1628
|
+
ckpt = torch.load(cw, map_location="cpu", weights_only=False)
|
|
1629
|
+
state_d = ckpt.get("state_dict", ckpt)
|
|
1630
|
+
load_matching_state_dict(base_model, state_d)
|
|
1631
|
+
if isinstance(ckpt, dict):
|
|
1632
|
+
global_step = int(ckpt.get("step", 0))
|
|
1633
|
+
chunk_idx = int(ckpt.get("chunk_idx", 0))
|
|
1634
|
+
best_loss = float(ckpt.get("best_loss", ckpt.get("loss", 999.0)))
|
|
1635
|
+
print(f"[+] Phục hồi Checkpoint đóng gói THÀNH CÔNG từ {cw}! Tiếp tục từ Step {global_step:,}, Chunk #{chunk_idx:,}, Best Loss {best_loss:.4f}")
|
|
1636
|
+
loaded_ckpt = True
|
|
1637
|
+
break
|
|
1638
|
+
except Exception as ex:
|
|
1639
|
+
print(f"[!] Lỗi nạp weights đóng gói: {ex}")
|
|
1640
|
+
|
|
1641
|
+
for attempt in range(1, 4):
|
|
1642
|
+
try:
|
|
1643
|
+
r_weights = requests.get(f"{active_url}/api/kaggle/stream/init_weights", timeout=120)
|
|
1644
|
+
if r_weights.status_code == 200 and len(r_weights.content) > 1000:
|
|
1645
|
+
ckpt = torch.load(io.BytesIO(r_weights.content), map_location="cpu", weights_only=False)
|
|
1646
|
+
state_d = ckpt.get("state_dict", ckpt)
|
|
1647
|
+
server_step = int(ckpt.get("step", 0)) if isinstance(ckpt, dict) else 0
|
|
1648
|
+
if server_step >= global_step:
|
|
1649
|
+
load_matching_state_dict(base_model, state_d)
|
|
1650
|
+
global_step = server_step
|
|
1651
|
+
chunk_idx = int(ckpt.get("chunk_idx", 0))
|
|
1652
|
+
best_loss = float(ckpt.get("best_loss", ckpt.get("loss", 999.0)))
|
|
1653
|
+
print(f"[+] Checkpoint mới nhất từ Server đồng bộ THÀNH CÔNG! Step {global_step:,}, Chunk #{chunk_idx:,}, Best Loss {best_loss:.4f}")
|
|
1654
|
+
loaded_ckpt = True
|
|
1655
|
+
break
|
|
1656
|
+
elif r_weights.status_code == 204:
|
|
1657
|
+
break
|
|
1658
|
+
except Exception as e:
|
|
1659
|
+
print(f"[*] Server checkpoint sync attempt {attempt}/3 ({e})")
|
|
1660
|
+
time.sleep(2)
|
|
1661
|
+
|
|
1662
|
+
session_start_time = time.time()
|
|
1663
|
+
session_initial_step = global_step
|
|
1664
|
+
|
|
1665
|
+
get_url_fn = lambda: resolve_current_tunnel_url(TUNNEL_URL)
|
|
1666
|
+
streamer_thread = threading.Thread(
|
|
1667
|
+
target=background_chunk_producer,
|
|
1668
|
+
args=(get_url_fn, SESSION_TOKEN, chunk_idx, streamer_stop_event, None),
|
|
1669
|
+
daemon=True
|
|
1670
|
+
)
|
|
1671
|
+
streamer_thread.start()
|
|
1672
|
+
print(f"[+] Background Streamer thread started from Chunk #{chunk_idx}!")
|
|
1673
|
+
|
|
1674
|
+
rolling_samples = 0
|
|
1675
|
+
rolling_compute_time = 0.0
|
|
1676
|
+
|
|
1677
|
+
while True:
|
|
1678
|
+
item = chunk_queue.get()
|
|
1679
|
+
if item is None:
|
|
1680
|
+
print("[+] All chunks processed. Finishing!")
|
|
1681
|
+
break
|
|
1682
|
+
|
|
1683
|
+
c_idx, dataset, cur_epoch, tot_epochs = item
|
|
1684
|
+
chunk_idx = c_idx
|
|
1685
|
+
cur_lr = compute_scheduled_lr(LEARNING_RATE, cur_epoch, tot_epochs)
|
|
1686
|
+
for pg in optimizer.param_groups:
|
|
1687
|
+
pg["lr"] = cur_lr
|
|
1688
|
+
print(f"\n[+] GPU Consumer [Epoch {cur_epoch}/{tot_epochs} | Auto-LR: {cur_lr:.6f}]: Chunk #{chunk_idx} ({len(dataset):,} mẫu)...")
|
|
1689
|
+
|
|
1690
|
+
micro_batch = min(256, BATCH_SIZE)
|
|
1691
|
+
dataloader = DataLoader(
|
|
1692
|
+
dataset,
|
|
1693
|
+
batch_size=micro_batch,
|
|
1694
|
+
shuffle=True,
|
|
1695
|
+
collate_fn=chunk_collate_fn,
|
|
1696
|
+
num_workers=2,
|
|
1697
|
+
pin_memory=not IS_TPU_DEVICE,
|
|
1698
|
+
drop_last=False
|
|
1699
|
+
)
|
|
1700
|
+
|
|
1701
|
+
base_model.train()
|
|
1702
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1703
|
+
grad_norm = 0.0
|
|
1704
|
+
|
|
1705
|
+
for batch_idx, batch in enumerate(dataloader):
|
|
1706
|
+
t_prep_0 = time.time()
|
|
1707
|
+
imgs = batch["images"].to(device, non_blocking=True)
|
|
1708
|
+
targets = batch["targets"].to(device, non_blocking=True)
|
|
1709
|
+
|
|
1710
|
+
if IS_TPU_DEVICE:
|
|
1711
|
+
target_lengths = batch["target_lengths"]
|
|
1712
|
+
input_lengths = torch.full((imgs.size(0),), imgs.size(3) // 4, dtype=torch.long, device="cpu")
|
|
1713
|
+
else:
|
|
1714
|
+
target_lengths = batch["target_lengths"].to(device, non_blocking=True)
|
|
1715
|
+
input_lengths = torch.full((imgs.size(0),), imgs.size(3) // 4, dtype=torch.long, device=device)
|
|
1716
|
+
t_prep = (time.time() - t_prep_0) * 1000
|
|
1717
|
+
|
|
1718
|
+
if IS_TPU_DEVICE and chunk_idx == 0 and batch_idx == 0:
|
|
1719
|
+
print("\n" + "-" * 60)
|
|
1720
|
+
print("[!] [XLA TPU] ĐANG BIÊN DỊCH ĐỒ THỊ TÍNH TOÁN (GRAPH COMPILATION)")
|
|
1721
|
+
print(" - Quá trình này sẽ sử dụng System RAM (chứ không dùng VRAM).")
|
|
1722
|
+
print(" - Có thể mất 2-5 phút cho lô (batch) đầu tiên.")
|
|
1723
|
+
print(" - Màn hình sẽ trông như bị treo, vui lòng KIÊN NHẪN CHỜ...")
|
|
1724
|
+
print("-" * 60)
|
|
1725
|
+
|
|
1726
|
+
t_fwd_0 = time.time()
|
|
1727
|
+
if IS_TPU_DEVICE:
|
|
1728
|
+
log_probs = base_model(imgs)
|
|
1729
|
+
t_fwd = (time.time() - t_fwd_0) * 1000
|
|
1730
|
+
t_loss_0 = time.time()
|
|
1731
|
+
|
|
1732
|
+
loss = ctc_criterion(log_probs, targets, input_lengths, target_lengths)
|
|
1733
|
+
|
|
1734
|
+
if chunk_idx == 0 and batch_idx == 0:
|
|
1735
|
+
print(" > [XLA TPU] Dịch xong Forward & Loss. Đang dịch Backward...")
|
|
1736
|
+
else:
|
|
1737
|
+
with torch.autocast(device_type="cuda", dtype=torch.float16):
|
|
1738
|
+
log_probs = base_model(imgs)
|
|
1739
|
+
t_fwd = (time.time() - t_fwd_0) * 1000
|
|
1740
|
+
t_loss_0 = time.time()
|
|
1741
|
+
loss = ctc_criterion(log_probs, targets, input_lengths, target_lengths)
|
|
1742
|
+
t_loss = (time.time() - t_loss_0) * 1000
|
|
1743
|
+
|
|
1744
|
+
if torch.isnan(loss) or torch.isinf(loss):
|
|
1745
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1746
|
+
continue
|
|
1747
|
+
|
|
1748
|
+
t_bwd_0 = time.time()
|
|
1749
|
+
if IS_TPU_DEVICE:
|
|
1750
|
+
loss.backward()
|
|
1751
|
+
import torch_xla.core.xla_model as xm
|
|
1752
|
+
# TPU gradient clipping MUST be done via xm, avoiding host-side float() syncs which crash/stall the compiler
|
|
1753
|
+
xm.optimizer_step(optimizer, barrier=True)
|
|
1754
|
+
# DO NOT use set_to_none=True on TPU, it forces XLA to reallocate gradient tensors and recompile the optimizer!
|
|
1755
|
+
optimizer.zero_grad()
|
|
1756
|
+
grad_norm = 0.0
|
|
1757
|
+
if chunk_idx == 0 and batch_idx == 0:
|
|
1758
|
+
print(" > [XLA TPU] HOÀN TẤT BIÊN DỊCH! Bắt đầu chạy tốc độ cao từ bây giờ!\n")
|
|
1759
|
+
else:
|
|
1760
|
+
scaler.scale(loss).backward()
|
|
1761
|
+
scaler.unscale_(optimizer)
|
|
1762
|
+
grad_norm = float(torch.nn.utils.clip_grad_norm_(base_model.parameters(), max_norm=2.0))
|
|
1763
|
+
scaler.step(optimizer)
|
|
1764
|
+
scaler.update()
|
|
1765
|
+
optimizer.zero_grad(set_to_none=True)
|
|
1766
|
+
t_bwd = (time.time() - t_bwd_0) * 1000
|
|
1767
|
+
|
|
1768
|
+
global_step += 1
|
|
1769
|
+
|
|
1770
|
+
loss_val = float(loss.item())
|
|
1771
|
+
|
|
1772
|
+
if loss_val < best_loss and global_step > 15:
|
|
1773
|
+
best_loss = loss_val
|
|
1774
|
+
sd_copy = {k: v.cpu().clone() for k, v in base_model.state_dict().items()}
|
|
1775
|
+
telemetry_pool.submit(async_save_checkpoint, resolve_current_tunnel_url(TUNNEL_URL), SESSION_TOKEN, global_step, chunk_idx, sd_copy, VOCAB_CHARS, loss_val, best_loss, 1)
|
|
1776
|
+
|
|
1777
|
+
step_samples = imgs.size(0)
|
|
1778
|
+
rolling_samples += step_samples
|
|
1779
|
+
rolling_compute_time += (t_prep + t_fwd + t_loss + t_bwd) / 1000.0
|
|
1780
|
+
if rolling_compute_time > 0.05:
|
|
1781
|
+
instant_sps = rolling_samples / rolling_compute_time
|
|
1782
|
+
else:
|
|
1783
|
+
instant_sps = 450.0
|
|
1784
|
+
|
|
1785
|
+
if rolling_samples > 1000:
|
|
1786
|
+
rolling_samples = rolling_samples // 2
|
|
1787
|
+
rolling_compute_time = rolling_compute_time / 2.0
|
|
1788
|
+
|
|
1789
|
+
if global_step <= 5 or global_step % 3 == 0:
|
|
1790
|
+
active_url = resolve_current_tunnel_url(TUNNEL_URL)
|
|
1791
|
+
# Dispatch standard telemetry
|
|
1792
|
+
gpus_telemetry = [{
|
|
1793
|
+
"index": 0,
|
|
1794
|
+
"name": dev_name,
|
|
1795
|
+
"used_mb": round(torch.cuda.memory_allocated(0) / (1024 * 1024), 1) if torch.cuda.is_available() else 0,
|
|
1796
|
+
"total_mb": vram_mb,
|
|
1797
|
+
"used_gb": round(torch.cuda.memory_allocated(0) / (1024 ** 3), 2) if torch.cuda.is_available() else 0,
|
|
1798
|
+
"total_gb": round(vram_mb / 1024.0, 2),
|
|
1799
|
+
"percent": round((torch.cuda.memory_allocated(0) / (1024 * 1024 * vram_mb)) * 100, 1) if (torch.cuda.is_available() and vram_mb > 0) else 0.0
|
|
1800
|
+
}]
|
|
1801
|
+
session_elapsed = int(time.time() - session_start_time)
|
|
1802
|
+
accumulated_session_samples = max(0, (global_step - session_initial_step)) * step_samples
|
|
1803
|
+
# Fetch predictions for UI using the already-on-CPU log_probs to prevent XLA sync
|
|
1804
|
+
with torch.no_grad():
|
|
1805
|
+
p_slice = log_probs[:, :min(3, log_probs.size(1)), :].cpu()
|
|
1806
|
+
p_texts = greedy_decode(p_slice)
|
|
1807
|
+
batch_texts = list(batch["texts"][:min(3, len(batch["texts"]))])
|
|
1808
|
+
batch_imgs = batch["images"][:min(3, len(batch["images"]))]
|
|
1809
|
+
img_b64s = [tensor_to_b64(img) for img in batch_imgs]
|
|
1810
|
+
|
|
1811
|
+
cer_val = 0.05
|
|
1812
|
+
try:
|
|
1813
|
+
cer_val = calculate_cer(batch_texts, p_texts)
|
|
1814
|
+
except Exception:
|
|
1815
|
+
pass
|
|
1816
|
+
|
|
1817
|
+
sample_preds = [{"truth": t, "pred": p, "correct": bool(t == p), "cer": round(calculate_cer([t], [p]), 3), "image_b64": b64} for t, p, b64 in zip(batch_texts, p_texts, img_b64s)]
|
|
1818
|
+
|
|
1819
|
+
telemetry_payload = {
|
|
1820
|
+
"session_token": SESSION_TOKEN,
|
|
1821
|
+
"step": global_step,
|
|
1822
|
+
"chunk_idx": chunk_idx,
|
|
1823
|
+
"epoch": cur_epoch,
|
|
1824
|
+
"total_epochs": tot_epochs,
|
|
1825
|
+
"num_gpus": 1,
|
|
1826
|
+
"step_samples": step_samples,
|
|
1827
|
+
"session_seconds": session_elapsed,
|
|
1828
|
+
"accumulated_session_samples": accumulated_session_samples,
|
|
1829
|
+
"lr": cur_lr,
|
|
1830
|
+
"loss": round(loss_val, 4),
|
|
1831
|
+
"best_loss": round(best_loss, 4),
|
|
1832
|
+
"cer": round(cer_val, 3),
|
|
1833
|
+
"samples_per_sec": round(instant_sps, 1),
|
|
1834
|
+
"device_name": dev_name,
|
|
1835
|
+
"vram_mb": gpus_telemetry[0]["used_mb"],
|
|
1836
|
+
"ram_mb": 0.0,
|
|
1837
|
+
"system_ram": {"used_mb": 0.0, "total_mb": 0.0, "used_gb": 0.0, "total_gb": 0.0, "percent": 0.0},
|
|
1838
|
+
"gpu_vram": {"count": 1, "gpus": gpus_telemetry, "used_mb": gpus_telemetry[0]["used_mb"], "total_mb": vram_mb, "used_gb": gpus_telemetry[0]["used_gb"], "total_gb": round(vram_mb / 1024.0, 2), "percent": gpus_telemetry[0]["percent"]},
|
|
1839
|
+
"sample_predictions": sample_preds,
|
|
1840
|
+
"neural_flow": {
|
|
1841
|
+
"active_node": "backward_pass",
|
|
1842
|
+
"tensor_shapes": {"input": f"[{imgs.shape[0]}, 3, 32, {imgs.shape[3]}]"},
|
|
1843
|
+
"node_latencies": {"data_prep": round(t_prep, 1), "forward_pass": round(t_fwd, 1), "ctc_loss": round(t_loss, 1), "backward_pass": round(t_bwd, 1)},
|
|
1844
|
+
"batch_mem_mb": round(float(imgs.element_size() * imgs.nelement()) / (1024 * 1024), 2),
|
|
1845
|
+
"batch_width": imgs.shape[3],
|
|
1846
|
+
"ctc_time_steps": log_probs.shape[0],
|
|
1847
|
+
"blank_rate": 62.5,
|
|
1848
|
+
"grad_norm": round(grad_norm, 2),
|
|
1849
|
+
"target_len_info": {"min": int(target_lengths.min().item()), "max": int(target_lengths.max().item()), "avg": round(float(target_lengths.float().mean().item()), 1)},
|
|
1850
|
+
"ctc_spectrogram": []
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
telemetry_pool.submit(async_post_telemetry, active_url, telemetry_payload)
|
|
1854
|
+
|
|
1855
|
+
del dataset
|
|
1856
|
+
del dataloader
|
|
1857
|
+
gc.collect()
|
|
1858
|
+
if torch.cuda.is_available():
|
|
1859
|
+
torch.cuda.empty_cache()
|
|
1860
|
+
|
|
1861
|
+
try:
|
|
1862
|
+
streamer_stop_event.set()
|
|
1863
|
+
except Exception:
|
|
1864
|
+
pass
|
|
1865
|
+
print("\n[+] Training session finished cleanly!")
|
|
1866
|
+
print("[+] Syncing final Checkpoint back to Local Studio...")
|
|
1867
|
+
sd_copy = {k: v.cpu().clone() for k, v in base_model.state_dict().items()}
|
|
1868
|
+
telemetry_pool.submit(async_save_checkpoint, resolve_current_tunnel_url(TUNNEL_URL), SESSION_TOKEN, global_step, chunk_idx, sd_copy, VOCAB_CHARS, best_loss, best_loss, 1).result()
|
|
1869
|
+
print("[+] Final Checkpoint synced successfully!")
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
# =========================================================================
|
|
1873
|
+
# 8. MAIN ENTRYPOINT
|
|
1874
|
+
# =========================================================================
|
|
1875
|
+
def main():
|
|
1876
|
+
num_gpus = torch.cuda.device_count() if torch.cuda.is_available() else 0
|
|
1877
|
+
if IS_TPU_DEVICE:
|
|
1878
|
+
print("=" * 70)
|
|
1879
|
+
print(" AsterOCR Cloud TPU Worker: Detected XLA Environment!")
|
|
1880
|
+
print(" Spawning PyTorch XLA Multi-Process (8 Cores)...")
|
|
1881
|
+
print("=" * 70)
|
|
1882
|
+
try:
|
|
1883
|
+
try:
|
|
1884
|
+
import torch_xla.distributed.xla_multiprocessing as xmp
|
|
1885
|
+
except ImportError:
|
|
1886
|
+
import torch_xla.distributed.xmp as xmp
|
|
1887
|
+
# Set environment variables for TPU
|
|
1888
|
+
os.environ['XLA_USE_BF16'] = '1'
|
|
1889
|
+
os.environ['XLA_TENSOR_ALLOCATOR_MAXSIZE'] = '1000000000'
|
|
1890
|
+
xmp.spawn(xla_ddp_worker, args=(), nprocs=None, start_method='fork')
|
|
1891
|
+
except Exception as e:
|
|
1892
|
+
import traceback
|
|
1893
|
+
print(f"[!] Lỗi xmp.spawn: {e}")
|
|
1894
|
+
traceback.print_exc()
|
|
1895
|
+
print("[!] Fallback to Single Device...")
|
|
1896
|
+
single_device_worker()
|
|
1897
|
+
elif num_gpus >= 2:
|
|
1898
|
+
print("=" * 70)
|
|
1899
|
+
print(f" AsterOCR Cloud GPU Worker: Detected {num_gpus}x GPUs!")
|
|
1900
|
+
print(f" Spawning PyTorch Multi-Process DistributedDataParallel (DDP)...")
|
|
1901
|
+
print("=" * 70)
|
|
1902
|
+
try:
|
|
1903
|
+
mp.set_start_method("spawn", force=True)
|
|
1904
|
+
except Exception:
|
|
1905
|
+
pass
|
|
1906
|
+
mp.spawn(ddp_train_worker, nprocs=num_gpus, args=(num_gpus,))
|
|
1907
|
+
else:
|
|
1908
|
+
single_device_worker()
|
|
1909
|
+
|
|
1910
|
+
if __name__ == "__main__":
|
|
1911
|
+
main()
|