cham-ocr-studio / ocr-training /scripts /orchestrate_v25_lightning.py
phucsd's picture
Upload folder using huggingface_hub
770f650 verified
Raw History Blame Contribute Delete
16.5 kB
#!/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()