Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -490,4 +490,8 @@ poetry.toml
# LSP config files
pyrightconfig.json

# End of https://www.toptal.com/developers/gitignore/api/python,latex
# End of https://www.toptal.com/developers/gitignore/api/python,latex
# Local training artefacts (fork)
/data/
/output/
/HunyuanOCR/
160 changes: 160 additions & 0 deletions docs/CUSTOM_SFT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# SFT HunyuanOCR-1.5 base trên dữ liệu tiếng Việt — 1 GPU 80GB

Runbook cho fork này. Nguồn dữ liệu: `makedata/hf_upload` (1.007 train / 120
validation, 4 config: GCN, Documents, Others, Handwriting).

Tất cả script upstream vẫn giữ nguyên hành vi cũ; phần riêng của fork là
`scripts/env_single.sh`, `scripts/sft_base_1gpu.sh`, `tools/makedata_to_hyocr.py`
và cờ `--allow-empty-answer` trong `tools/pipeline_count_and_pack.py`.

---

## 0. Môi trường

```bash
conda create -n hyocr python=3.12 -y && conda activate hyocr
pip install -r requirements.txt
pip install flash-attn --no-build-isolation # bắt buộc, xem ghi chú bên dưới
pip install binpacking lmdb
```

`train/train_hunyuan.py` gọi model với `attn_implementation="flash_attention_2"`
cứng trong code, **không có đường lùi sang eager** — thiếu `flash-attn` là chết
ngay lúc load model, không phải lúc train.

Tải model gốc:

```bash
hf download tencent/HunyuanOCR --local-dir ./HunyuanOCR
export MODEL_PATH=$PWD/HunyuanOCR
```

## 1. Chuyển dữ liệu sang định dạng raw JSONL

```bash
python tools/makedata_to_hyocr.py \
--root /home/jaqja/New_AI/makedata/hf_upload \
--prompt-file /home/jaqja/New_AI/makedata/ocr_prompt.md \
--out-dir ./data/raw \
--data-list ./data/data_list.txt
```

Sinh ra `data/raw/train.jsonl` (1.007 dòng) và `data/raw/validation.jsonl`
(120 dòng), mỗi dòng:

```json
{"img_path_sh": "/abs/000.jpg", "conv": [{"question": "<prompt>", "answer": "<markdown>"}]}
```

> **`docs/data_format.md` của upstream mô tả sai.** Doc ghi schema
> `{"image_path": [...], "conversations": [{"from","value"}]}` và packed output
> `{"packed_samples", "cu_seqlens", "total_tokens"}`. Không có dòng code nào
> trong repo đọc các key đó. `tools/pipeline_count_and_pack.py:157-184` đọc
> `img_path_sh` / `img_path_cq` + `conv[0].question` / `conv[0].answer`, và
> `train/data_processor.py:153-189` đọc `item["image"]` / `["question"]` /
> `["answer"]` với mỗi dòng packed là **một JSON array**. Converter bám theo code.

Prompt nằm ở `question`, không phải `system`: hàm pack không chuyển
`--system-prompt` vào bản ghi packed, nên system prompt được **tính token lúc
pack rồi biến mất lúc train** — đặt prompt ở đó là lệch dữ liệu.

## 2. Pack

```bash
MODEL_PATH=$MODEL_PATH \
INPUT_LIST=./data/data_list.txt \
PACK_OUTPUT=./data/packed/train_16384.jsonl \
PACK_LEN=16384 \
NUM_PROCESSES=8 THREADS_PER_PROCESS=8 \
FOREGROUND=1 \
bash scripts/pack_data.sh
```

`ALLOW_EMPTY=1` là mặc định của fork. Upstream loại thẳng mọi mẫu có `answer`
rỗng (`if not question or not answer: return None`) — với bộ này là **25 trang
trắng** cố ý gán nhãn rỗng để dạy model im lặng. Loại chúng đi thì dạy đúng điều
ngược lại. Đặt `ALLOW_EMPTY=0` nếu muốn hành vi upstream.

Kiểm tra kết quả:

```bash
python -c "
import json
n=t=0
for line in open('./data/packed/train_16384.jsonl'):
p=json.loads(line); n+=1; t+=sum(x['num_tokens'] for x in p)
print(f'{n} pack, trung bình {t/n:.0f} token/pack')"
```

Mỗi trang A4 200 DPI (1656×2339 ≈ 3,87 Mpx, dưới ngưỡng `max_pixels`
= 2048² nên **không bị resize**) tốn phần lớn ngân sách token là vision token.
Nếu số pack ra quá ít (mỗi pack chỉ 2–3 trang) thì cân nhắc `--max-pixels`
nhỏ hơn khi pack — đổi lại chữ nhỏ trên giấy sẽ mờ đi.

## 3. Train

```bash
MODEL_PATH=$MODEL_PATH \
TRAIN_DATA=./data/packed/train_16384.jsonl \
PACK_LEN=16384 \
bash scripts/sft_base_1gpu.sh
```

Khác gì so với `scripts/sft_base.sh`:

| | `sft_base.sh` | `sft_base_1gpu.sh` |
|---|---|---|
| GPU | 8 | **1** |
| Env | `env_common.sh` (IB, `bond1`, 8 HCA mlx5) | **`env_single.sh`** (không IB) |
| `packed_max_length` | 20480 | **16384** |
| `GRAD_ACCUM` | 1 | **4** |
| `SAVE_STEPS` | 200 | **50** |
| `warmup_ratio` | 0.03 | 0.03 |
| DeepSpeed | không | tuỳ chọn qua `DEEPSPEED=scripts/zero2.json` |

Model chỉ ~1B tham số nên ở bf16 + `gradient_checkpointing` thì 80GB thừa sức
chạy full SFT (vision + MLP + LLM) mà **không cần ZeRO**. Biến còn lại ăn VRAM là
`PACK_LEN`; upstream ghi rằng 20480 vẫn OOM được trên 80GB nên fork lấy 16384 làm
mặc định. Nếu `nvidia-smi` cho thấy còn nhiều headroom, nâng dần:

```bash
PACK_LEN=20480 bash scripts/sft_base_1gpu.sh # nhớ pack lại data cùng độ dài
```

`PACK_LEN` lúc train **phải bằng** `PACK_LEN` lúc pack.

Các nút khác:

```bash
TUNE_VISION=False ... # đóng băng vision tower, nhanh hơn ~15%
EPOCHS=3 LR=1e-5 ...
DEEPSPEED=scripts/zero2.json ... # chỉ khi OOM sau khi đã hạ PACK_LEN
```

Train tự resume: nếu `output_dir` đã có `checkpoint-*` thì
`train_hunyuan.py:221` gọi `trainer.train(resume_from_checkpoint=True)`. Muốn
chạy lại từ đầu thì đổi `RUN_NAME` hoặc xoá thư mục.

## 4. Đánh giá

**Không bật `--eval_strategy steps`.** `train_hunyuan.py:198-205` tạo
`eval_dataset` với `is_packed=False` (trả về dict lẻ) nhưng collator lúc đó là
`PackedVLDataCollator` (chờ list-of-list) → vỡ ngay batch eval đầu tiên. Đánh giá
tách rời bằng `data/raw/validation.jsonl` và code inference trong `inference/`.

## 5. Những chỗ đã sửa của upstream

| File | Sửa |
|---|---|
| `scripts/sft_base.sh:106`, `sft_dflash.sh:127`, `sft_dflash_finetune.sh:140` | `${entry_file} "${args}"` → bỏ nháy. Có nháy thì toàn bộ flag thành **một** phần tử `argv`, `HfArgumentParser.parse_args_into_dataclasses()` chết ngay. |
| `tools/pipeline_count_and_pack.py` | thêm `--allow-empty-answer` |
| `scripts/pack_data.sh` | thêm `ALLOW_EMPTY`, `FOREGROUND` |

## 6. Ghi chú về quy mô

Bộ này có 1.007 trang train. Recipe của Tencent dùng ~1M pack cho pretrain và
14,7k pack cho domain finetune, tức là ta ít hơn khoảng hai bậc. Kỳ vọng thực tế
là **thích nghi domain** (đúng bố cục biểu mẫu VN, đúng quy ước Markdown trong
`ocr_prompt.md`), không phải nâng năng lực OCR tổng quát. Nếu loss train tụt
nhanh mà chất lượng trên validation không lên, đó là overfit — hạ `EPOCHS`
xuống 2–3 và/hoặc đóng băng vision tower trước khi nghĩ tới việc đổi LR.
22 changes: 22 additions & 0 deletions scripts/env_single.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
# ============================================================================
# Environment for SINGLE-NODE / SINGLE-GPU training.
#
# Use this instead of env_common.sh when there is no InfiniBand fabric.
# env_common.sh pins NCCL to a `bond1` interface and eight mlx5 HCAs; on a
# single box those devices do not exist and NCCL either warns loudly or hangs
# during rendezvous.
# ============================================================================

# No IB, no multi-NIC: keep NCCL on loopback and let it fall back to shared mem.
export NCCL_IB_DISABLE=1
export NCCL_P2P_DISABLE=0
export NCCL_SOCKET_IFNAME=${NCCL_SOCKET_IFNAME:-lo}
export NCCL_DEBUG=${NCCL_DEBUG:-WARN}
export NCCL_ASYNC_ERROR_HANDLING=1

# Tokenizers forks inside the dataloader workers; silence the parallelism warning.
export TOKENIZERS_PARALLELISM=false

# Less fragmentation when activations swing between packs of different length.
export PYTORCH_CUDA_ALLOC_CONF=${PYTORCH_CUDA_ALLOC_CONF:-expandable_segments:True}
37 changes: 24 additions & 13 deletions scripts/pack_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ PACK_OUTPUT=${PACK_OUTPUT:-./data/parsing_packed_${PACK_LEN}.jsonl}
NUM_PROCESSES=${NUM_PROCESSES:-32}
THREADS_PER_PROCESS=${THREADS_PER_PROCESS:-8}
LOG_FILE=${LOG_FILE:-pack_data.log}
# Keep blank-page samples (empty answer). Set ALLOW_EMPTY=0 to drop them.
ALLOW_EMPTY=${ALLOW_EMPTY:-1}
EMPTY_FLAG=""
[ "$ALLOW_EMPTY" = "1" ] && EMPTY_FLAG="--allow-empty-answer"
# Run in the foreground when FOREGROUND=1 (easier to watch on one machine).
FOREGROUND=${FOREGROUND:-0}

# ────────────── Sanity check ──────────────
if [ ! -f "$INPUT_LIST" ]; then
Expand Down Expand Up @@ -48,16 +54,21 @@ echo " Log : $LOG_FILE"
echo "========================================"

# ────────────── Run ──────────────
nohup python tools/pipeline_count_and_pack.py \
--input-list "$INPUT_LIST" \
--model-path "$MODEL_PATH" \
--count-output-dir "$COUNT_OUTPUT_DIR" \
--pack-output "$PACK_OUTPUT" \
--num-processes "$NUM_PROCESSES" \
--threads-per-process "$THREADS_PER_PROCESS" \
--pack-length "$PACK_LEN" \
> "$LOG_FILE" 2>&1 &

PID=$!
echo "[started] pid=$PID log=$LOG_FILE"
echo "Monitor with: tail -f $LOG_FILE"
cmd=(python tools/pipeline_count_and_pack.py
--input-list "$INPUT_LIST"
--model-path "$MODEL_PATH"
--count-output-dir "$COUNT_OUTPUT_DIR"
--pack-output "$PACK_OUTPUT"
--num-processes "$NUM_PROCESSES"
--threads-per-process "$THREADS_PER_PROCESS"
--pack-length "$PACK_LEN")
[ -n "$EMPTY_FLAG" ] && cmd+=("$EMPTY_FLAG")

if [ "$FOREGROUND" = "1" ]; then
"${cmd[@]}" 2>&1 | tee "$LOG_FILE"
else
nohup "${cmd[@]}" > "$LOG_FILE" 2>&1 &
PID=$!
echo "[started] pid=$PID log=$LOG_FILE"
echo "Monitor with: tail -f $LOG_FILE"
fi
2 changes: 1 addition & 1 deletion scripts/sft_base.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@ torchrun --nproc_per_node="${NPROC_PER_NODE}" \
--master_port="${MASTER_PORT}" \
--node_rank="${NODE_RANK}" \
--nnodes="${NNODES}" \
${entry_file} "${args}" 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
${entry_file} ${args} 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
116 changes: 116 additions & 0 deletions scripts/sft_base_1gpu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/bin/bash
# ============================================================================
# SFT the HunyuanOCR base model on a SINGLE 80GB GPU (A100-80 / H100).
#
# Same entry point as scripts/sft_base.sh, retuned for one GPU and for a small
# domain dataset (~1k pages) instead of a cluster and ~1M packs:
#
# NPROC_PER_NODE 8 -> 1
# env_common.sh -> env_single.sh (no InfiniBand on a single box)
# packed_max_length 20480 -> 16384 (upstream notes 20480 OOMs at 80GB)
# GRAD_ACCUM 1 -> 4 (1 pack/step is too noisy a batch)
# SAVE_STEPS 200 -> 50 (a whole epoch is well under 200)
#
# Everything is still env-overridable, e.g. PACK_LEN=20480 EPOCHS=3 bash ...
# ============================================================================

set -e

# ────────────── Common env ──────────────
source scripts/env_single.sh

# ────────────── Distributed configuration ──────────────
MASTER_ADDR=${MASTER_ADDR:-"127.0.0.1"}
MASTER_PORT=${MASTER_PORT:-$(shuf -i 20001-29999 -n 1)}
NNODES=${NNODES:-1}
NPROC_PER_NODE=${NPROC_PER_NODE:-1}
NODE_RANK=${NODE_RANK:-0}

# ────────────── Model & data paths ──────────────
model_name_or_path=${MODEL_PATH:?set MODEL_PATH to the HunyuanOCR base model dir}
train_data_path=${TRAIN_DATA:-./data/packed/train_16384.jsonl}
image_path="not_needed" # packed records carry absolute image paths

# ────────────── Hyperparameters ──────────────
lr=${LR:-2e-5}
batch_size=${BATCH_SIZE:-1}
grad_accum_steps=${GRAD_ACCUM:-4}
num_epochs=${EPOCHS:-5}
warmup_ratio=${WARMUP:-0.03}
save_steps=${SAVE_STEPS:-50}
pack_len=${PACK_LEN:-16384}

# Which parts to train. Freezing the vision tower (TUNE_VISION=False) saves
# roughly 15% of step time and is worth trying if the pages are clean scans.
tune_vision=${TUNE_VISION:-True}
tune_mlp=${TUNE_MLP:-True}
tune_llm=${TUNE_LLM:-True}

# Optional ZeRO-2. Not needed for a 1B model at 80GB; set DEEPSPEED=scripts/zero2.json
# only if you hit OOM after lowering PACK_LEN.
deepspeed_arg=""
[ -n "${DEEPSPEED}" ] && deepspeed_arg="--deepspeed ${DEEPSPEED}"

entry_file=train/train_hunyuan.py

# ────────────── Output ──────────────
run_name=${RUN_NAME:-"hyocr_sft_1gpu_lr${lr}_ep${num_epochs}_$(date +%m%d_%H%M)"}
output_dir=${OUTPUT_DIR:-"./output/${run_name}"}
TENSORBOARD_DIR="${output_dir}/tensorboard/$(date "+%Y.%m.%d-%H.%M.%S")"
mkdir -p "${TENSORBOARD_DIR}"

echo "========================================"
echo "Run name : ${run_name}"
echo "Output dir : ${output_dir}"
echo "Base model : ${model_name_or_path}"
echo "Train data : ${train_data_path}"
echo "----- Hyperparameters -----"
echo " gpus : ${NPROC_PER_NODE}"
echo " lr : ${lr}"
echo " epochs : ${num_epochs}"
echo " batch x accum : ${batch_size} x ${grad_accum_steps}"
echo " pack length : ${pack_len}"
echo " tune v/m/l : ${tune_vision} / ${tune_mlp} / ${tune_llm}"
echo " deepspeed : ${DEEPSPEED:-off}"
echo "========================================"

# ────────────── Training args ──────────────
args="
--model_name_or_path ${model_name_or_path} \
--train_data_path ${train_data_path} \
--image_folder ${image_path} \
--data_flatten True \
--data_packing True \
--tune_mm_vision ${tune_vision} \
--tune_mm_mlp ${tune_mlp} \
--tune_mm_llm ${tune_llm} \
--bf16 \
--output_dir ${output_dir} \
--num_train_epochs ${num_epochs} \
--per_device_train_batch_size ${batch_size} \
--per_device_eval_batch_size ${batch_size} \
--gradient_accumulation_steps ${grad_accum_steps} \
--eval_strategy no \
--save_strategy steps \
--save_steps ${save_steps} \
--save_total_limit 3 \
--learning_rate ${lr} \
--weight_decay 0.01 \
--warmup_ratio ${warmup_ratio} \
--max_grad_norm 1 \
--lr_scheduler_type cosine_with_min_lr \
--logging_steps 5 \
--packed_max_length ${pack_len} \
--gradient_checkpointing True \
--dataloader_num_workers 4 \
--run_name ${run_name} \
--logging_dir ${TENSORBOARD_DIR} \
--report_to tensorboard ${deepspeed_arg}"

# ────────────── Launch ──────────────
torchrun --nproc_per_node="${NPROC_PER_NODE}" \
--master_addr="${MASTER_ADDR}" \
--master_port="${MASTER_PORT}" \
--node_rank="${NODE_RANK}" \
--nnodes="${NNODES}" \
${entry_file} ${args} 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
2 changes: 1 addition & 1 deletion scripts/sft_dflash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@ torchrun --nproc_per_node="${NPROC_PER_NODE}" \
--master_port="${MASTER_PORT}" \
--node_rank="${NODE_RANK}" \
--nnodes="${NNODES}" \
${entry_file} "${args}" 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
${entry_file} ${args} 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
2 changes: 1 addition & 1 deletion scripts/sft_dflash_finetune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,4 @@ torchrun --nproc_per_node="${NPROC_PER_NODE}" \
--master_port="${MASTER_PORT}" \
--node_rank="${NODE_RANK}" \
--nnodes="${NNODES}" \
${entry_file} "${args}" 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
${entry_file} ${args} 2>&1 | tee "${output_dir}/train_${NODE_RANK}.log"
Loading