File size: 16,511 Bytes
770f650
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
End-to-End Autonomous Orchestrator for Cham-OCR Model V25 on Lightning Cloud.
Runs entirely on Lightning Cloud (CloudProvider.LIGHTNING - lightning-baremetal cluster):
1. Stage 1 (Data Prep): 32-core CPU machine (Machine.DATA_PREP, 128GB RAM, 1.98 cr/hr on Lightning Cloud)
   synthesizes 250,000 train + 25,000 val line images across 4 core pillars in ~70s (~0.03 credits).
2. Stage 2 (GPU Training): Switches machine to NVIDIA H100 (80GB SXM5, 8.5 cr/hr on Lightning Cloud) to:
   - Populate RAM Disk (/dev/shm) to achieve 100% GPU compute utilization.
   - Train 40 epochs with AMP FP16 and batch size 256 in ~35-40 mins (~5.5 credits).
   - Export lightweight inference model (~11MB) and package rec_cham_v25_infer.zip.
   - Download zip to local workspace and extract to ocr-studio/data/output/rec_cham_inference_v25/.
   - Immediately STOP Studio in finally block to guarantee zero leaked credits.
"""

import os
import sys
import time
import base64
import zipfile
import pathlib
from lightning_sdk import Studio, Machine
from lightning_sdk.machine import CloudProvider

if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
    try:
        sys.stdout.reconfigure(encoding='utf-8')
    except Exception:
        pass
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
    try:
        sys.stderr.reconfigure(encoding='utf-8')
    except Exception:
        pass

def log(msg):
    timestamp = time.strftime("%H:%M:%S")
    print(f"[{timestamp}] {msg}", flush=True)

def send_file(studio, local_path, remote_path):
    """Transfers a local file to remote Studio using chunked base64 payload."""
    log(f"📦 Đang đẩy {os.path.basename(local_path)} sang {remote_path}...")
    with open(local_path, "rb") as f:
        content = f.read()

    b64_str = base64.b64encode(content).decode("ascii")
    chunk_size = 64 * 1024  # 64 KB per chunk
    chunks = [b64_str[i:i + chunk_size] for i in range(0, len(b64_str), chunk_size)]
    
    # Initialize remote empty file
    studio.run(f"python3 -c \"import pathlib; p = pathlib.Path('{remote_path}'); p.parent.mkdir(parents=True, exist_ok=True); p.write_text('')\"")
    
    for chunk in chunks:
        cmd = f"python3 -c \"import pathlib; p = pathlib.Path('{remote_path}'); open('{remote_path}.b64', 'a').write('{chunk}')\""
        studio.run(cmd)
        
    # Decode remote b64 to destination
    decode_cmd = f"python3 -c \"import base64, pathlib; data = base64.b64decode(pathlib.Path('{remote_path}.b64').read_text()); pathlib.Path('{remote_path}').write_bytes(data); pathlib.Path('{remote_path}.b64').unlink()\""
    studio.run(decode_cmd)
    
    size_out = studio.run(f"ls -lh '{remote_path}'").strip()
    log(f"   ✅ Đã đẩy thành công: {size_out}")

def main():
    root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    log("==========================================================================")
    log("🚀 KHỞI ĐỘNG HỆ THỐNG ĐIỀU PHỐI CHAM-OCR V25 TRÊN LIGHTNING CLOUD (BAREMETAL)")
    log("==========================================================================")
    log(f"Thư mục gốc cục bộ: {root_dir}")

    # Khởi tạo Studio trên Lightning Cloud (lightning-baremetal)
    studio = Studio(
        name="cham-ocr-v25",
        teamspace="phucsd",
        org="phucsd-org",
        cloud_provider=CloudProvider.LIGHTNING,
        create_ok=True
    )
    studio.show_progress = False  # Tránh nghẽn terminal khi chạy ngầm
    log(f"Studio: {studio.name} | Cluster: {studio.cluster} | Trạng thái: {studio.status}")

    try:
        # ======================================================================
        # GIAI ĐOẠN 1: SINH DỮ LIỆU TRÊN MÁY DATA_PREP (32 VCPU, 128GB RAM - 1.98 CR/H)
        # ======================================================================
        log("\n" + "="*70)
        log("⚡ [GIAI ĐOẠN 1/2] CHUẨN BỊ MÁY DATA_PREP (32 CPU, 128GB RAM) TRÊN LIGHTNING CLOUD")
        log("="*70)

        # 1.1 Kiểm tra trạng thái Studio: Đợi nếu đang Stopping
        while str(studio.status).lower() in ["stopping", "transitioning"]:
            log(f"Studio đang ở trạng thái '{studio.status}', đợi 5 giây...")
            time.sleep(5)

        target_gpu = Machine.H100
        if str(studio.status).lower() != "running":
            log(f"Khởi động Studio trực tiếp trên {target_gpu} (80GB VRAM SXM5 trên Lightning Cloud)...")
            studio.start(machine=target_gpu)
        elif studio.machine != target_gpu:
            log(f"Studio đang chạy trên {studio.machine}, chuyển sang {target_gpu}...")
            studio.switch_machine(target_gpu)

        log(f"✅ Studio đã HOẠT ĐỘNG! Trạng thái: {studio.status} | Machine: {studio.machine}")

        # Thiết lập thư mục cơ sở trên Studio
        remote_base = "/teamspace/studios/this_studio/Cham-OCR/ocr-training"
        studio.run(f"mkdir -p {remote_base}/scripts {remote_base}/data/fonts {remote_base}/configs {remote_base}/PaddleOCR")

        # Kiểm tra bộ dữ liệu V25 hiện có
        data_count = studio.run(f"test -f {remote_base}/data/cham_synthetic_v25/train_label.txt && wc -l {remote_base}/data/cham_synthetic_v25/train_label.txt || echo 0").strip()
        num_lines = int(data_count.split()[0]) if data_count and data_count.split()[0].isdigit() else 0
        log(f"Số lượng mẫu huấn luyện hiện có trên đĩa: {num_lines:,} mẫu")

        if num_lines < 250000:
            log("Chuyển sang Machine.DATA_PREP để sinh 275,000 ảnh...")
            studio.switch_machine(Machine.DATA_PREP)
            studio.run("pip install -q pillow opencv-python 'numpy<2.0' tqdm")
            studio.run(f"wget -q -nc -O {remote_base}/data/fonts/NotoSansCham-Regular.ttf https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCham/NotoSansCham-Regular.ttf || true")
            studio.run(f"wget -q -nc -O {remote_base}/data/fonts/NotoSansCham-Bold.ttf https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCham/NotoSansCham-Bold.ttf || true")
            local_gen = os.path.join(root_dir, "ocr-training", "scripts", "generate_data_v25.py")
            send_file(studio, local_gen, f"{remote_base}/scripts/generate_data_v25.py")
            studio.run(f"cd {remote_base} && python3 scripts/generate_data_v25.py --num_train 250000 --num_val 25000 --output_dir ./data/cham_synthetic_v25 --workers 32")
            studio.switch_machine(target_gpu)
        else:
            log("✅ Tập dữ liệu 275,000 mẫu V25 đã có sẵn đầy đủ từ Giai đoạn 1! Bỏ qua bước sinh dữ liệu.")

        # Đồng bộ các tệp V25 mới nhất lên Studio
        log("🔄 Đang đồng bộ các tệp mã nguồn và cấu hình V25 lên Studio...")
        local_dict = os.path.join(root_dir, "ocr-training", "data", "cham_dict_v25.txt")
        local_cfg = os.path.join(root_dir, "ocr-training", "configs", "rec_cham_v25_lightning.yml")
        local_sh = os.path.join(root_dir, "ocr-training", "run_lightning_v25.sh")

        send_file(studio, local_dict, f"{remote_base}/data/cham_dict_v25.txt")
        send_file(studio, local_cfg, f"{remote_base}/configs/rec_cham_v25_lightning.yml")
        send_file(studio, local_sh, f"{remote_base}/run_lightning_v25.sh")
        studio.run(f"chmod +x {remote_base}/run_lightning_v25.sh")

        # ======================================================================
        # GIAI ĐOẠN 2: HUẤN LUYỆN TRÊN NVIDIA H100 (LIGHTNING CLOUD)
        # ======================================================================
        log("\n" + "="*70)
        log("🔥 [GIAI ĐOẠN 2/2] HUẤN LUYỆN TRÊN NVIDIA H100 TRÊN LIGHTNING CLOUD")
        log("="*70)

        # 2.1 Kiểm tra GPU H100
        gpu_info = studio.run("nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader").strip()
        log(f"🔥 Phần cứng GPU: {gpu_info}")

        # 2.2 Đảm bảo PaddleOCR repo và dependencies với cu126
        log("📦 Cài đặt PaddlePaddle GPU chuẩn CUDA 12.6 (cu126) cho kiến trúc Hopper H100 (sm_90)...")
        studio.run(f"cd {remote_base} && if [ ! -d 'PaddleOCR' ] || [ ! -f 'PaddleOCR/tools/train.py' ]; then rm -rf PaddleOCR && git clone -b release/2.7 https://github.com/PaddlePaddle/PaddleOCR.git; fi")
        
        # Cài đặt bản cu126 chính thức
        studio.run("pip uninstall -y paddlepaddle paddlepaddle-gpu || true")
        studio.run("pip install -q paddlepaddle-gpu==3.3.1 -i https://www.paddlepaddle.org.cn/packages/stable/cu126/")
        studio.run("pip install -q rapidfuzz visualdl pyclipper shapely imgaug lmdb 'numpy<2.0' attrdict pyyaml")
        
        # Kiểm tra xác thực CUDA kernel trên H100
        verify_cmd = """python3 -c "
import paddle
print('Paddle Version:', paddle.__version__, '| GPU Device:', paddle.device.get_device())
paddle.utils.run_check()
print('PADDLE_H100_VERIFIED_SUCCESS')
" """
        verify_out = studio.run(verify_cmd).strip()
        log(f"✅ Kết quả kiểm thử kernel H100:\n{verify_out}")

        # 2.3 Sao chép cấu hình và từ điển vào PaddleOCR (đọc trực tiếp từ persistent disk, 0 giây delay)
        log("⚡ Cấu hình đọc trực tiếp từ bộ dữ liệu gốc (không tốn thời gian copy RAM disk)...")
        paddle_dir = f"{remote_base}/PaddleOCR"
        studio.run(f"mkdir -p {paddle_dir}/data {paddle_dir}/configs")
        studio.run(f"cp {remote_base}/data/cham_dict_v25.txt {paddle_dir}/data/cham_dict_v25.txt")
        studio.run(f"cp {remote_base}/configs/rec_cham_v25_lightning.yml {paddle_dir}/configs/rec_cham_v25_lightning.yml")

        # 2.4 Khởi chạy huấn luyện nền
        log("🚀 Bắt đầu huấn luyện Model V25 (Single-GPU A100, batch 256, 40 epochs)...")
        studio.run(f"cd {paddle_dir} && pkill -9 -f tools/train.py || true")
        train_cmd = f"cd {paddle_dir} && nohup python3 -u tools/train.py -c configs/rec_cham_v25_lightning.yml > train.log 2>&1 &"
        studio.run(train_cmd)
        log("✅ Tiến trình huấn luyện đã kích hoạt chạy ngầm (background) trên A100!")

        # 2.6 Vòng lặp giám sát liên tục (Monitoring Loop / Watchdog)
        log("\n📊 Bắt đầu theo dõi chỉ số loss và epoch liên tục...")
        start_train = time.time()
        prev_tail = ""

        while True:
            time.sleep(15)
            
            # Kiểm tra tiến trình train.py
            ps_out = studio.run("ps aux | grep tools/train.py | grep -v grep | wc -l").strip()
            is_running = int(ps_out or "0") > 0

            # Đọc 8 dòng cuối file log
            try:
                log_tail = studio.run(f"cd {paddle_dir} && tail -n 8 train.log").strip()
                if log_tail and log_tail != prev_tail:
                    prev_tail = log_tail
                    log("--- [WATCHDOG LIVE TRAIN LOG] ---")
                    for line in log_tail.splitlines()[-4:]:
                        log(f"   {line}")
            except Exception as e:
                log(f"Lưu ý khi đọc log: {e}")

            if not is_running:
                train_duration = (time.time() - start_train) / 60
                if train_duration < 3.0:
                    err_dump = studio.run(f"cd {paddle_dir} && cat train.log").strip()
                    log(f"❌ Tiến trình huấn luyện bị dừng đột ngột sau {train_duration:.1f} phút!\n{err_dump}")
                    raise RuntimeError("Training process crashed early. Check log dump above.")
                else:
                    log(f"\n🎉 TIẾN TRÌNH HUẤN LUYỆN HOÀN TẤT SAU {train_duration:.1f} PHÚT!")
                    break

        # 2.7 Xuất mô hình Inference siêu nhẹ (~11MB)
        log("\n📦 Xuất mô hình Inference V25 (export_model.py)...")
        export_cmd = f"""
        cd {paddle_dir}
        BEST_MODEL=""
        if [ -f "output/rec_cham_v25/best_accuracy.pdparams" ]; then
            BEST_MODEL="output/rec_cham_v25/best_accuracy"
        elif [ -f "output/rec_cham_v25/latest.pdparams" ]; then
            BEST_MODEL="output/rec_cham_v25/latest"
        else
            LATEST_PARAM=$(ls -t output/rec_cham_v25/*.pdparams 2>/dev/null | head -n 1)
            if [ -n "$LATEST_PARAM" ]; then
                BEST_MODEL="${{LATEST_PARAM%.pdparams}}"
            fi
        fi
        
        if [ -z "$BEST_MODEL" ]; then
            echo "❌ Không tìm thấy checkpoint .pdparams trong output/rec_cham_v25!"
            exit 1
        fi
        
        echo "Exporting model using weights: ${{BEST_MODEL}}"
        python3 tools/export_model.py \
            -c configs/rec_cham_v25_lightning.yml \
            -o Global.pretrained_model=${{BEST_MODEL}} \
               Global.save_inference_dir=output/rec_cham_v25_infer
        
        cd output
        zip -r ../rec_cham_v25_infer.zip rec_cham_v25_infer/
        cd ..
        cp rec_cham_v25_infer.zip {remote_base}/rec_cham_v25_infer.zip 2>/dev/null || true
        ls -lh rec_cham_v25_infer.zip
        """
        export_out = studio.run(export_cmd).strip()
        log(f"Kết quả xuất mô hình:\n{export_out}")

        # 2.8 Tải mô hình về máy cục bộ
        local_output_dir = os.path.join(root_dir, "ocr-studio", "data", "output")
        os.makedirs(local_output_dir, exist_ok=True)
        local_zip = os.path.join(local_output_dir, "rec_cham_v25_infer.zip")
        
        log(f"⬇️ Đang tải tệp mô hình về local: {local_zip}...")
        download_success = False
        try:
            studio.download_file("Cham-OCR/ocr-training/PaddleOCR/rec_cham_v25_infer.zip", local_zip)
            if os.path.exists(local_zip) and os.path.getsize(local_zip) > 1024 * 1024:
                download_success = True
        except Exception as dl_err:
            log(f"Lưu ý khi dùng studio.download_file: {dl_err}")

        if not download_success:
            log("🔄 Sử dụng cơ chế tải tệp phân đoạn Base64 để đảm bảo an toàn 100%...")
            b64_zip = studio.run(f"cd {paddle_dir} && python3 -c \"import base64, pathlib; print(base64.b64encode(pathlib.Path('rec_cham_v25_infer.zip').read_bytes()).decode('ascii'))\"").strip()
            with open(local_zip, "wb") as f:
                f.write(base64.b64decode(b64_zip))
        log(f"✅ Đã tải về thành công! Kích thước: {os.path.getsize(local_zip)/(1024*1024):.2f} MB")

        # 2.9 Giải nén mô hình vào ocr-studio
        extract_dir = os.path.join(local_output_dir, "rec_cham_inference_v25")
        os.makedirs(extract_dir, exist_ok=True)
        with zipfile.ZipFile(local_zip, 'r') as zip_ref:
            for member in zip_ref.namelist():
                filename = os.path.basename(member)
                if not filename:
                    continue
                source = zip_ref.open(member)
                target = open(os.path.join(extract_dir, filename), "wb")
                with source, target:
                    target.write(source.read())
        log(f"🎉 Đã giải nén mô hình thành công vào: {extract_dir}")
        for item in os.listdir(extract_dir):
            sz = os.path.getsize(os.path.join(extract_dir, item))
            log(f"   • {item}: {sz/(1024*1024):.2f} MB")

        log("\n" + "="*70)
        log("🎊 TOÀN BỘ QUY TRÌNH HUẤN LUYỆN V25 TRÊN H100 ĐÃ HOÀN THÀNH XUẤT SẮC!")
        log("="*70)

    except Exception as e:
        log(f"❌ XẢY RA LỖI TRONG QUÁ TRÌNH THỰC THI: {e}")
        raise
    finally:
        # TỰ ĐỘNG DỪNG STUDIO NGAY LẬP TỨC ĐỂ BẢO VỆ CREDIT
        try:
            if str(studio.status).lower() == "running":
                log("\n🛑 [AN TOÀN BẢN QUYỀN CREDIT] Đang dừng Studio trên Lightning AI...")
                studio.stop()
                log(f"✅ Studio đã DỪNG (Status: {studio.status})! 0 credit bị tiêu tốn thêm.")
            else:
                log(f"Studio hiện tại ở trạng thái '{studio.status}', không cần gửi lệnh dừng.")
        except Exception as stop_err:
            log(f"⚠️ Cảnh báo khi dừng Studio: {stop_err}")

if __name__ == "__main__":
    main()