Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- config/__pycache__/config.cpython-310.pyc +0 -0
- config/__pycache__/config.cpython-312.pyc +0 -0
- config/__pycache__/config_trimode.cpython-312.pyc +0 -0
- config/config.py +193 -0
- config/config_change.py +79 -0
- config/config_trimode_antidegen.py +61 -0
- config/loader.py +66 -0
- configs/deepspeed/zero2_bf16.json +40 -0
- data_utils/__pycache__/paths.cpython-310.pyc +0 -0
- data_utils/aokvqa/download.py +279 -0
- data_utils/aokvqa/evaluator.py +21 -0
- data_utils/aokvqa/patch_aokvqa.py +101 -0
- data_utils/chart/__pycache__/evaluator.cpython-310.pyc +0 -0
- data_utils/chart/pre_refinement.py +134 -0
- data_utils/chart/prompts.py +114 -0
- data_utils/clevr/clevr_processor.py +461 -0
- data_utils/clevr/run_single_test.py +194 -0
- data_utils/paths.py +58 -0
- default_config_zero2.yaml +20 -0
- main_7B.py +192 -0
- opsd_utils/__init__.py +18 -0
- opsd_utils/__pycache__/__init__.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/__init__.cpython-312.pyc +0 -0
- opsd_utils/__pycache__/constants.cpython-312.pyc +0 -0
- opsd_utils/__pycache__/debug_log.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/health_monitor.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/leakage.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/mode_router.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/mode_router.cpython-312.pyc +0 -0
- opsd_utils/__pycache__/opsd_loss.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/prompt_builder.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/recoverability.cpython-310.pyc +0 -0
- opsd_utils/__pycache__/recoverability.cpython-312.pyc +0 -0
- opsd_utils/__pycache__/vocab_align.cpython-310.pyc +0 -0
- opsd_utils/constants.py +40 -0
- opsd_utils/debug_log.py +426 -0
- opsd_utils/deepspeed_utils.py +141 -0
- opsd_utils/diagnostics.py +1351 -0
- opsd_utils/leakage.py +53 -0
- opsd_utils/opsd_loss.py +439 -0
- opsd_utils/privileged/__pycache__/__init__.cpython-310.pyc +0 -0
- opsd_utils/privileged/__pycache__/__init__.cpython-312.pyc +0 -0
- opsd_utils/privileged/__pycache__/base.cpython-310.pyc +0 -0
- opsd_utils/privileged/__pycache__/base.cpython-312.pyc +0 -0
- opsd_utils/privileged/__pycache__/image_utils.cpython-310.pyc +0 -0
- opsd_utils/privileged/__pycache__/providers.cpython-312.pyc +0 -0
- opsd_utils/privileged/__pycache__/registry.cpython-310.pyc +0 -0
- opsd_utils/privileged/__pycache__/registry.cpython-312.pyc +0 -0
- opsd_utils/privileged/debug_artifacts.py +104 -0
- opsd_utils/privileged/providers.py +127 -0
config/__pycache__/config.cpython-310.pyc
ADDED
|
Binary file (3.71 kB). View file
|
|
|
config/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (3.16 kB). View file
|
|
|
config/__pycache__/config_trimode.cpython-312.pyc
ADDED
|
Binary file (1.06 kB). View file
|
|
|
config/config.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from data_utils.paths import OUTPUTS_DIR, project_path
|
| 5 |
+
|
| 6 |
+
# ====== Model Configuration ======
|
| 7 |
+
MODEL_CONFIG = {
|
| 8 |
+
"pretrained_model_path": "llava-hf/llava-onevision-qwen2-0.5b-ov-hf", # two-stage grpo
|
| 9 |
+
"use_flash_attention_2": True,
|
| 10 |
+
"torch_dtype": "bfloat16",
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
# ====== Training Configuration ======
|
| 14 |
+
TRAINING_CONFIG = {
|
| 15 |
+
"task": 'chart',
|
| 16 |
+
"num_gpus": 8, # 使用的 GPU 数量
|
| 17 |
+
"num_client": 8, # 并发客户端数量,通常与 GPU 数量相同
|
| 18 |
+
# RL阶段的参数 (根据原脚本的rl_args)
|
| 19 |
+
"dyme_args": {
|
| 20 |
+
"output_dir": os.path.join(OUTPUTS_DIR, "dyme-k-8"),
|
| 21 |
+
"logging_steps": 1,
|
| 22 |
+
"num_generations": 8, # RL 阶段可以生成多个响应进行比较
|
| 23 |
+
"max_completion_length": 200,
|
| 24 |
+
"temperature": 0.8,
|
| 25 |
+
"repetition_penalty": 1.15,
|
| 26 |
+
"per_device_train_batch_size": 2,
|
| 27 |
+
"gradient_accumulation_steps": 16,
|
| 28 |
+
"num_train_epochs": 10,
|
| 29 |
+
"learning_rate": 8e-5,
|
| 30 |
+
"bf16": True, # 使用 bf16 而不是 fp16
|
| 31 |
+
"gradient_checkpointing": False,
|
| 32 |
+
"ddp_find_unused_parameters": False,
|
| 33 |
+
"max_grad_norm": 1.0,
|
| 34 |
+
"save_strategy": "epoch",
|
| 35 |
+
"weight_decay": 0.01,
|
| 36 |
+
"warmup_steps": 0,
|
| 37 |
+
"beta": 0.0, # DyMETrainer requires beta=0 (no reference-model KL path yet)
|
| 38 |
+
"loss_type": 'grpo', # GRPO specific
|
| 39 |
+
"seed": 42,
|
| 40 |
+
},
|
| 41 |
+
"sft_args": {
|
| 42 |
+
"output_dir": os.path.join(OUTPUTS_DIR, "sft-chart-llava_cot"),
|
| 43 |
+
"logging_steps": 1,
|
| 44 |
+
"per_device_train_batch_size": 2,
|
| 45 |
+
"gradient_accumulation_steps": 4,
|
| 46 |
+
"num_train_epochs": 10,
|
| 47 |
+
"learning_rate": 1e-5,
|
| 48 |
+
"bf16": True, # 使用 bf16 而不是 fp16
|
| 49 |
+
"gradient_checkpointing": False,
|
| 50 |
+
"ddp_find_unused_parameters": False,
|
| 51 |
+
"max_grad_norm": 1.0,
|
| 52 |
+
# "save_steps": 100,
|
| 53 |
+
"save_strategy": "epoch",
|
| 54 |
+
"weight_decay": 0.01,
|
| 55 |
+
"warmup_steps": 0,
|
| 56 |
+
"seed": 42,
|
| 57 |
+
"remove_unused_columns": False
|
| 58 |
+
},
|
| 59 |
+
"grpo_args":{
|
| 60 |
+
"output_dir": os.path.join(OUTPUTS_DIR, "grpo-chart-llava-beta"),
|
| 61 |
+
"logging_steps": 1,
|
| 62 |
+
"num_generations": 4, # RL 阶段可以生成多个响应进行比较
|
| 63 |
+
"max_completion_length": 576,
|
| 64 |
+
"max_prompt_length": None,
|
| 65 |
+
"per_device_train_batch_size": 2,
|
| 66 |
+
"gradient_accumulation_steps": 4,
|
| 67 |
+
"num_train_epochs": 10,
|
| 68 |
+
"learning_rate": 1e-5,
|
| 69 |
+
"bf16": True, # 使用 bf16 而不是 fp16
|
| 70 |
+
"gradient_checkpointing": False,
|
| 71 |
+
"ddp_find_unused_parameters": False,
|
| 72 |
+
"max_grad_norm": 1.0,
|
| 73 |
+
"save_strategy": "epoch",
|
| 74 |
+
"weight_decay": 0.01,
|
| 75 |
+
"warmup_steps": 0,
|
| 76 |
+
"beta": 0.04, # GRPO specific
|
| 77 |
+
"loss_type": 'grpo', # GRPO specific
|
| 78 |
+
"seed": 42,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
RL_CONFIG = {
|
| 84 |
+
"answer_flag": "Answer:",
|
| 85 |
+
"end_flag": "<|im_end|>"
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# OPSD / TriMode training (disabled by default -> original DyME behavior)
|
| 89 |
+
DYME_OPSD_CONFIG = {
|
| 90 |
+
"enabled": False,
|
| 91 |
+
"mode": "dyme",
|
| 92 |
+
"privileged_profile": "hybrid",
|
| 93 |
+
"privileged_providers": ["text"],
|
| 94 |
+
"privileged_image": {
|
| 95 |
+
"mode": "single",
|
| 96 |
+
"crop_strategy": "bbox_then_center",
|
| 97 |
+
"bbox_coord": "normalized",
|
| 98 |
+
"margin_ratio": 0.25,
|
| 99 |
+
},
|
| 100 |
+
"privileged_debug": {
|
| 101 |
+
"save_images": True,
|
| 102 |
+
"image_subdir": "logs/images",
|
| 103 |
+
"max_samples_per_detail": 2,
|
| 104 |
+
},
|
| 105 |
+
"gate": {
|
| 106 |
+
"correct_threshold": 0.5,
|
| 107 |
+
"teacher_recoverable": "privileged_available",
|
| 108 |
+
"recoverable_tau": 0.5,
|
| 109 |
+
"use_edge_mask": False,
|
| 110 |
+
# TriMode: OPSD only on completions that are correct + well-formatted (not whole group).
|
| 111 |
+
"per_completion_opsd": True,
|
| 112 |
+
"require_format_for_opsd": True,
|
| 113 |
+
"skip_degenerate_for_opsd": True,
|
| 114 |
+
},
|
| 115 |
+
"loss": {
|
| 116 |
+
"beta": 0.5,
|
| 117 |
+
"opsd_weight": 2.0,
|
| 118 |
+
"grpo_weight": 1.0,
|
| 119 |
+
"sft_weight": 1.0,
|
| 120 |
+
},
|
| 121 |
+
# Optional: [format, context, accuracy] — default uniform; antidegen uses [0.5, 1.5, 1.0]
|
| 122 |
+
"reward_weights": [1.0, 1.0, 1.0],
|
| 123 |
+
"debug": {
|
| 124 |
+
# Full weak-signal diagnostic bundle every N global steps (rank 0). 0 = off.
|
| 125 |
+
"detail_every": 10,
|
| 126 |
+
# Lightweight [OPSD-PROBE] on every (re)generate (rank 0). Independent of OPSD-DEBUG.
|
| 127 |
+
"probe_on_generate": False,
|
| 128 |
+
"probe_sample_count": 4,
|
| 129 |
+
# Deep generate diagnostics ([OPSD-GENDBG]) — prompt tail, first-token logits, model context.
|
| 130 |
+
"probe_first_token_logits": True,
|
| 131 |
+
"probe_prompt_tail_tokens": 16,
|
| 132 |
+
"probe_log_model_context": True,
|
| 133 |
+
"health_monitor": {
|
| 134 |
+
"enabled": True,
|
| 135 |
+
"window": 20,
|
| 136 |
+
"log_on_generate": True,
|
| 137 |
+
"log_every_step": True,
|
| 138 |
+
"log_detail_bundle": True,
|
| 139 |
+
"log_alerts_immediately": True,
|
| 140 |
+
"metrics_every_step": True,
|
| 141 |
+
},
|
| 142 |
+
},
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# ====== Client Configuration for Reward Calculation ======
|
| 146 |
+
CLIENT_CONFIG = {
|
| 147 |
+
"client_type": "openai", # 客户端主机地址
|
| 148 |
+
"api_key": "none", # 客户端主机
|
| 149 |
+
"api_base": "http://127.0.0.1:%s/v1", # 客户端,如果是本地服务需要预留端口
|
| 150 |
+
"timeout": 60, # 请求超时时间
|
| 151 |
+
"model_id": "Qwen/Qwen2.5-14B-Instruct-AWQ", # 使用的模型ID
|
| 152 |
+
"init_port": 23333, # 或者none代表在线服务
|
| 153 |
+
"num_server": 8
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
# ====== Dataset Configuration ======
|
| 157 |
+
DATASET_CONFIG = {
|
| 158 |
+
"train_dataset": project_path("data/chartqa/train_medium_vf_full.json"),
|
| 159 |
+
# 训练数据路径
|
| 160 |
+
"eval_dataset": "HuggingFaceM4/ChartQA", # 验证数据路径
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
# ====== DePlot offline preprocessing (ChartQA F2) ======
|
| 164 |
+
DEPLOT_CONFIG = {
|
| 165 |
+
"enabled": True,
|
| 166 |
+
"model_id": "google/deplot",
|
| 167 |
+
"batch_size": 8,
|
| 168 |
+
"max_new_tokens": 384,
|
| 169 |
+
"cache_path": project_path("data/chartqa/deplot_cache.json"),
|
| 170 |
+
"prompt": "Generate underlying data table of the figure below:",
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
# ====== Full Configuration ======
|
| 174 |
+
CONFIG = {
|
| 175 |
+
"model": MODEL_CONFIG,
|
| 176 |
+
"training": TRAINING_CONFIG,
|
| 177 |
+
"rl": RL_CONFIG,
|
| 178 |
+
"opsd": DYME_OPSD_CONFIG,
|
| 179 |
+
"client": CLIENT_CONFIG,
|
| 180 |
+
"dataset": DATASET_CONFIG,
|
| 181 |
+
"deplot": DEPLOT_CONFIG,
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
# Save configuration to a file for reference
|
| 185 |
+
def save_config(config, config_path="./config.json"):
|
| 186 |
+
import json
|
| 187 |
+
with open(config_path, "w") as f:
|
| 188 |
+
json.dump(config, f, indent=4)
|
| 189 |
+
|
| 190 |
+
# Example usage to save config
|
| 191 |
+
if __name__ == "__main__":
|
| 192 |
+
save_config(CONFIG)
|
| 193 |
+
|
config/config_change.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
# ====== Model Configuration ======
|
| 5 |
+
MODEL_CONFIG = {
|
| 6 |
+
"pretrained_model_path": "llava-hf/llava-onevision-qwen2-0.5b-ov-hf", # two-stage grpo
|
| 7 |
+
"use_flash_attention_2": True,
|
| 8 |
+
"torch_dtype": "bfloat16",
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
# ====== Training Configuration ======
|
| 12 |
+
TRAINING_CONFIG = {
|
| 13 |
+
"task": 'chart',
|
| 14 |
+
"num_gpus": 8,
|
| 15 |
+
"num_client": 8,
|
| 16 |
+
|
| 17 |
+
"dyme_args": {
|
| 18 |
+
"output_dir": '/path/to/dyme-llavaov-chart-change3',
|
| 19 |
+
"logging_steps": 1,
|
| 20 |
+
"num_generations": 4,
|
| 21 |
+
"max_completion_length": 200,
|
| 22 |
+
"per_device_train_batch_size": 1,
|
| 23 |
+
"gradient_accumulation_steps": 16, # norm 16; 20 for sft hybrid
|
| 24 |
+
"num_train_epochs": 10,
|
| 25 |
+
"learning_rate": 8e-5,
|
| 26 |
+
"bf16": True,
|
| 27 |
+
"gradient_checkpointing": False,
|
| 28 |
+
"ddp_find_unused_parameters": False,
|
| 29 |
+
"max_grad_norm": 1.0,
|
| 30 |
+
"save_strategy": "epoch",
|
| 31 |
+
"weight_decay": 0.01,
|
| 32 |
+
"warmup_steps": 0,
|
| 33 |
+
"beta": 0.0, # GRPO specific
|
| 34 |
+
"loss_type": 'grpo', # GRPO specific
|
| 35 |
+
"seed": 42,
|
| 36 |
+
},
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
RL_CONFIG = {
|
| 40 |
+
"answer_flag": "Answer:",
|
| 41 |
+
"end_flag": "<|im_end|>"
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# ====== Client Configuration for Reward Calculation ======
|
| 45 |
+
CLIENT_CONFIG = {
|
| 46 |
+
"client_type": "openai",
|
| 47 |
+
"api_key": "none", # 客户端主机
|
| 48 |
+
"api_base": "http://127.0.0.1:%s/v1",
|
| 49 |
+
"timeout": 60,
|
| 50 |
+
"model_id": "Qwen/Qwen2.5-14B-Instruct-AWQ",
|
| 51 |
+
"init_port": 23333,
|
| 52 |
+
"num_server": 8
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# ====== Dataset Configuration ======
|
| 56 |
+
DATASET_CONFIG = {
|
| 57 |
+
"train_dataset": "/path/to/data/chartqa_output/json/train_new_prerefine.json",
|
| 58 |
+
"eval_dataset": "HuggingFaceM4/ChartQA",
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# ====== Full Configuration ======
|
| 62 |
+
CONFIG = {
|
| 63 |
+
"model": MODEL_CONFIG,
|
| 64 |
+
"training": TRAINING_CONFIG,
|
| 65 |
+
"rl": RL_CONFIG,
|
| 66 |
+
"client": CLIENT_CONFIG,
|
| 67 |
+
"dataset": DATASET_CONFIG,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# Save configuration to a file for reference
|
| 71 |
+
def save_config(config, config_path="./config.json"):
|
| 72 |
+
import json
|
| 73 |
+
with open(config_path, "w") as f:
|
| 74 |
+
json.dump(config, f, indent=4)
|
| 75 |
+
|
| 76 |
+
# Example usage to save config
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
save_config(CONFIG)
|
| 79 |
+
|
config/config_trimode_antidegen.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
TriMode ChartQA config with anti-degeneration overrides (log-validated).
|
| 3 |
+
|
| 4 |
+
Based on train_trimode_4gpu_20260610_173637.log analysis:
|
| 5 |
+
- decoding: shorter max length, lower temperature, higher repetition penalty
|
| 6 |
+
- training: lower LR + warmup to mitigate step-1 collapse
|
| 7 |
+
- OPSD gate: require_format_for_opsd=False to raise opsd_mask coverage
|
| 8 |
+
- reward_weights: emphasize continuous context F1 for group reward spread
|
| 9 |
+
"""
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
import config.config_trimode as trimode
|
| 13 |
+
from data_utils.paths import OUTPUTS_DIR
|
| 14 |
+
|
| 15 |
+
MODEL_CONFIG = dict(trimode.MODEL_CONFIG)
|
| 16 |
+
|
| 17 |
+
TRAINING_CONFIG = {
|
| 18 |
+
**trimode.TRAINING_CONFIG,
|
| 19 |
+
"dyme_args": {
|
| 20 |
+
**trimode.TRAINING_CONFIG["dyme_args"],
|
| 21 |
+
"output_dir": os.environ.get(
|
| 22 |
+
"DYME_OUTPUT_DIR",
|
| 23 |
+
os.path.join(OUTPUTS_DIR, "dyme-trimode-antidegen"),
|
| 24 |
+
),
|
| 25 |
+
"max_completion_length": 150,
|
| 26 |
+
"temperature": 0.7,
|
| 27 |
+
"repetition_penalty": 1.25,
|
| 28 |
+
"learning_rate": 5e-5,
|
| 29 |
+
"warmup_steps": 50,
|
| 30 |
+
},
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
_reward_weights_raw = os.environ.get("DYME_REWARD_WEIGHTS", "0.5,1.5,1.0")
|
| 34 |
+
try:
|
| 35 |
+
_reward_weights = [float(x.strip()) for x in _reward_weights_raw.split(",") if x.strip()]
|
| 36 |
+
if len(_reward_weights) != 3:
|
| 37 |
+
raise ValueError("expected 3 weights")
|
| 38 |
+
except ValueError:
|
| 39 |
+
_reward_weights = [0.5, 1.5, 1.0]
|
| 40 |
+
|
| 41 |
+
_require_format_raw = os.environ.get("DYME_OPSD_REQUIRE_FORMAT", "0").strip().lower()
|
| 42 |
+
_require_format_for_opsd = _require_format_raw not in ("0", "false", "no", "off")
|
| 43 |
+
|
| 44 |
+
DYME_OPSD_CONFIG = {
|
| 45 |
+
**trimode.DYME_OPSD_CONFIG,
|
| 46 |
+
"gate": {
|
| 47 |
+
**trimode.DYME_OPSD_CONFIG.get("gate", {}),
|
| 48 |
+
"require_format_for_opsd": _require_format_for_opsd,
|
| 49 |
+
"skip_degenerate_for_opsd": True,
|
| 50 |
+
},
|
| 51 |
+
"reward_weights": _reward_weights,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
CONFIG = {
|
| 55 |
+
"model": MODEL_CONFIG,
|
| 56 |
+
"training": TRAINING_CONFIG,
|
| 57 |
+
"rl": trimode.RL_CONFIG,
|
| 58 |
+
"opsd": DYME_OPSD_CONFIG,
|
| 59 |
+
"client": trimode.CLIENT_CONFIG,
|
| 60 |
+
"dataset": trimode.DATASET_CONFIG,
|
| 61 |
+
}
|
config/loader.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
import importlib.util
|
| 3 |
+
import os
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
_CONFIG_ALIASES: dict[str, str] = {
|
| 7 |
+
"norm": "config.config",
|
| 8 |
+
"trimode": "config.config_trimode",
|
| 9 |
+
"trimode_antidegen": "config.config_trimode_antidegen",
|
| 10 |
+
"rlsd": "config.config_rlsd_chartqa",
|
| 11 |
+
"rlsd_chartqa": "config.config_rlsd_chartqa",
|
| 12 |
+
"opd_7b": "config.config_opd_7b_chartqa",
|
| 13 |
+
"opd_7b_chartqa": "config.config_opd_7b_chartqa",
|
| 14 |
+
"llavacot": "config.config_llavacot",
|
| 15 |
+
"low": "config.config_low",
|
| 16 |
+
"aok": "config.config_aok",
|
| 17 |
+
"change": "config.config_change",
|
| 18 |
+
"7b": "config.config_7B",
|
| 19 |
+
"llm": "config.config_llm",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _resolve_config_path(config_arg: str) -> str:
|
| 24 |
+
if os.path.isfile(config_arg):
|
| 25 |
+
return os.path.abspath(config_arg)
|
| 26 |
+
|
| 27 |
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 28 |
+
candidate = os.path.join(project_root, config_arg)
|
| 29 |
+
if os.path.isfile(candidate):
|
| 30 |
+
return os.path.abspath(candidate)
|
| 31 |
+
|
| 32 |
+
raise FileNotFoundError(f"Config file not found: {config_arg}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_config(config_arg: str) -> dict[str, Any]:
|
| 36 |
+
"""
|
| 37 |
+
Load CONFIG from a Python config file path or shorthand alias.
|
| 38 |
+
|
| 39 |
+
Examples:
|
| 40 |
+
load_config("config/config.py")
|
| 41 |
+
load_config("config/config_trimode.py")
|
| 42 |
+
load_config("norm")
|
| 43 |
+
load_config("trimode")
|
| 44 |
+
"""
|
| 45 |
+
if config_arg.endswith(".py"):
|
| 46 |
+
path = _resolve_config_path(config_arg)
|
| 47 |
+
module_name = f"_dyme_runtime_config_{abs(hash(path)) % 10 ** 8}"
|
| 48 |
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
| 49 |
+
if spec is None or spec.loader is None:
|
| 50 |
+
raise ImportError(f"Cannot load config module from {path}")
|
| 51 |
+
module = importlib.util.module_from_spec(spec)
|
| 52 |
+
spec.loader.exec_module(module)
|
| 53 |
+
if not hasattr(module, "CONFIG"):
|
| 54 |
+
raise ValueError(f"{path} must define a CONFIG dict")
|
| 55 |
+
return module.CONFIG
|
| 56 |
+
|
| 57 |
+
module_path = _CONFIG_ALIASES.get(config_arg)
|
| 58 |
+
if module_path is None:
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"Unknown config alias '{config_arg}'. "
|
| 61 |
+
f"Use a .py path (e.g. config/config.py) or one of: {', '.join(sorted(_CONFIG_ALIASES))}"
|
| 62 |
+
)
|
| 63 |
+
module = importlib.import_module(module_path)
|
| 64 |
+
if not hasattr(module, "CONFIG"):
|
| 65 |
+
raise ValueError(f"{module_path} must define a CONFIG dict")
|
| 66 |
+
return module.CONFIG
|
configs/deepspeed/zero2_bf16.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bf16": {
|
| 3 |
+
"enabled": "auto"
|
| 4 |
+
},
|
| 5 |
+
"optimizer": {
|
| 6 |
+
"type": "AdamW",
|
| 7 |
+
"params": {
|
| 8 |
+
"lr": "auto",
|
| 9 |
+
"betas": "auto",
|
| 10 |
+
"eps": "auto",
|
| 11 |
+
"weight_decay": "auto",
|
| 12 |
+
"torch_adam": true,
|
| 13 |
+
"adam_w_mode": true
|
| 14 |
+
}
|
| 15 |
+
},
|
| 16 |
+
"scheduler": {
|
| 17 |
+
"type": "WarmupDecayLR",
|
| 18 |
+
"params": {
|
| 19 |
+
"warmup_min_lr": "auto",
|
| 20 |
+
"warmup_max_lr": "auto",
|
| 21 |
+
"warmup_num_steps": "auto",
|
| 22 |
+
"total_num_steps": "auto"
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"zero_optimization": {
|
| 26 |
+
"stage": 2,
|
| 27 |
+
"allgather_partitions": true,
|
| 28 |
+
"allgather_bucket_size": 200000000.0,
|
| 29 |
+
"overlap_comm": true,
|
| 30 |
+
"reduce_scatter": true,
|
| 31 |
+
"reduce_bucket_size": "auto",
|
| 32 |
+
"contiguous_gradients": true
|
| 33 |
+
},
|
| 34 |
+
"gradient_accumulation_steps": "auto",
|
| 35 |
+
"gradient_clipping": "auto",
|
| 36 |
+
"train_batch_size": "auto",
|
| 37 |
+
"train_micro_batch_size_per_gpu": "auto",
|
| 38 |
+
"steps_per_print": 2000,
|
| 39 |
+
"wall_clock_breakdown": false
|
| 40 |
+
}
|
data_utils/__pycache__/paths.cpython-310.pyc
ADDED
|
Binary file (1.57 kB). View file
|
|
|
data_utils/aokvqa/download.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import base64
|
| 4 |
+
import io
|
| 5 |
+
import multiprocessing
|
| 6 |
+
from functools import partial
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from datasets import load_dataset
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
from openai import OpenAI, BadRequestError
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
from data_utils.paths import AOKVQA_DIR
|
| 14 |
+
|
| 15 |
+
# Ensure PIL can handle large images
|
| 16 |
+
Image.MAX_IMAGE_PIXELS = None
|
| 17 |
+
|
| 18 |
+
# --- API and MLLM prompt configuration ---
|
| 19 |
+
|
| 20 |
+
API_PORTS = list(range(23333, 23333 + 8))
|
| 21 |
+
API_URL_TEMPLATE = "http://127.0.0.1:{port}/v1/"
|
| 22 |
+
|
| 23 |
+
# 2. Define prompts used to obtain visual_fact
|
| 24 |
+
VISUAL_FACT_SYSTEM_PROMPT = """
|
| 25 |
+
You are a helpful assistant that analyzes images and provides visual facts.
|
| 26 |
+
Your response MUST be a single, valid JSON object.
|
| 27 |
+
The JSON object should contain:
|
| 28 |
+
1. "description": A detailed and accurate description of the image.
|
| 29 |
+
2. "objects": A list of key objects, including their name, attributes, and approximate position in the image.
|
| 30 |
+
|
| 31 |
+
Example format:
|
| 32 |
+
{
|
| 33 |
+
"description": "A person riding a bicycle on a city street.... (detailed description here)",
|
| 34 |
+
"objects": [
|
| 35 |
+
{"name": "person", "attributes": ["wearing helmet", "blue shirt"], "position": "center"},
|
| 36 |
+
{"name": "bicycle", "attributes": ["red", "mountain bike"], "position": "center"},
|
| 37 |
+
{"name": "street", "attributes": ["asphalt", "wet"], "position": "bottom"}
|
| 38 |
+
]
|
| 39 |
+
}
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
VISUAL_FACT_USER_PROMPT = """
|
| 43 |
+
Analyze the attached image and provide the visual facts in the required JSON format.
|
| 44 |
+
For context, the user will be asked this question about the image (do not answer the question, just use it for context):
|
| 45 |
+
"{question}"
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def encode_image_to_base64(pil_image):
|
| 50 |
+
"""Convert a PIL Image object to a Base64-encoded string"""
|
| 51 |
+
buffered = io.BytesIO()
|
| 52 |
+
if pil_image.mode == "RGBA" or "transparency" in pil_image.info:
|
| 53 |
+
pil_image.save(buffered, format="PNG")
|
| 54 |
+
mime_type = "image/png"
|
| 55 |
+
else:
|
| 56 |
+
pil_image.save(buffered, format="JPEG")
|
| 57 |
+
mime_type = "image/jpeg"
|
| 58 |
+
|
| 59 |
+
img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 60 |
+
return f"data:{mime_type};base64,{img_str}"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def get_visual_fact(api_url, pil_image, question):
|
| 64 |
+
"""
|
| 65 |
+
Call an external MLLM API to obtain visual_fact.
|
| 66 |
+
"""
|
| 67 |
+
try:
|
| 68 |
+
client = OpenAI(base_url=api_url, api_key="DUMMY_KEY")
|
| 69 |
+
image_url = encode_image_to_base64(pil_image)
|
| 70 |
+
|
| 71 |
+
messages = [
|
| 72 |
+
{"role": "system", "content": VISUAL_FACT_SYSTEM_PROMPT},
|
| 73 |
+
{
|
| 74 |
+
"role": "user",
|
| 75 |
+
"content": [
|
| 76 |
+
{"type": "image_url", "image_url": {"url": image_url,}},
|
| 77 |
+
{"type": "text", "text": VISUAL_FACT_USER_PROMPT.format(question=question)}
|
| 78 |
+
]
|
| 79 |
+
}
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
response = client.chat.completions.create(
|
| 84 |
+
model="Qwen/Qwen2.5-VL-32B-Instruct-AWQ",
|
| 85 |
+
messages=messages,
|
| 86 |
+
max_tokens=1024,
|
| 87 |
+
temperature=0.0,
|
| 88 |
+
)
|
| 89 |
+
response_content = response.choices[0].message.content
|
| 90 |
+
return response_content
|
| 91 |
+
|
| 92 |
+
except (BadRequestError, Exception):
|
| 93 |
+
response = client.chat.completions.create(
|
| 94 |
+
model="Qwen/Qwen2.5-VL-32B-Instruct-AWQ",
|
| 95 |
+
messages=messages,
|
| 96 |
+
max_tokens=1024,
|
| 97 |
+
temperature=0.0,
|
| 98 |
+
)
|
| 99 |
+
response_content = response.choices[0].message.content
|
| 100 |
+
|
| 101 |
+
if response_content.startswith("```json"):
|
| 102 |
+
response_content = response_content[7:].strip("` \n")
|
| 103 |
+
|
| 104 |
+
return response_content
|
| 105 |
+
|
| 106 |
+
except json.JSONDecodeError as e:
|
| 107 |
+
print(f"!! JSON parse failed: {e}. API: {api_url}. Raw response: {response_content[:100]}...")
|
| 108 |
+
return {"error": "Failed to parse JSON response", "raw_response": response_content}
|
| 109 |
+
except Exception as e:
|
| 110 |
+
print(f"!! API call failed: {e}. API: {api_url}")
|
| 111 |
+
time.sleep(1)
|
| 112 |
+
return {"error": f"API call failed: {str(e)}"}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# --- (This is the modified function) ---
|
| 116 |
+
def process_example_worker(example_with_index, split, image_output_dir, api_ports_list, fetch_visual_facts):
|
| 117 |
+
"""
|
| 118 |
+
Worker function for multiprocessing.
|
| 119 |
+
Process a single example: save image and (optionally) call the API.
|
| 120 |
+
"""
|
| 121 |
+
i, example = example_with_index
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
# 1. Extract metadata
|
| 125 |
+
question = example["question"]
|
| 126 |
+
pil_image = example["image"]
|
| 127 |
+
|
| 128 |
+
# --- (A) Extract new fields ---
|
| 129 |
+
choices_list = example.get("choices")
|
| 130 |
+
correct_idx = example.get("correct_choice_idx")
|
| 131 |
+
direct_answers_list = example.get("direct_answers")
|
| 132 |
+
rationales_list = example.get("rationales") # A-OKVQA has a 'rationales' field
|
| 133 |
+
|
| 134 |
+
# --- (B) Determine "answer" (prefer choice as requested) ---
|
| 135 |
+
answer = None
|
| 136 |
+
if choices_list and correct_idx is not None and 0 <= correct_idx < len(choices_list):
|
| 137 |
+
answer = choices_list[correct_idx]
|
| 138 |
+
elif direct_answers_list:
|
| 139 |
+
answer = direct_answers_list[0] # Fallback to direct_answers
|
| 140 |
+
|
| 141 |
+
# --- (C) Determine "hint" (the longest rationale) ---
|
| 142 |
+
hint = None
|
| 143 |
+
if rationales_list:
|
| 144 |
+
try:
|
| 145 |
+
# Ensure rationales_list is a non-empty list of strings
|
| 146 |
+
if isinstance(rationales_list, list) and len(rationales_list) > 0:
|
| 147 |
+
string_rationales = [r for r in rationales_list if isinstance(r, str)]
|
| 148 |
+
if string_rationales:
|
| 149 |
+
hint = max(string_rationales, key=len) # Choose the longest string
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"!! Warning: Failed to compute 'hint' (index {i}). Error: {e}")
|
| 152 |
+
pass # hint remains None
|
| 153 |
+
|
| 154 |
+
# --- (D) End of extraction ---
|
| 155 |
+
# 2. Generate and save image filename
|
| 156 |
+
generated_filename = f"{split}_{i:07d}.png"
|
| 157 |
+
image_save_path = os.path.join(image_output_dir, generated_filename)
|
| 158 |
+
|
| 159 |
+
# 3. Save image (I/O operation)
|
| 160 |
+
if not os.path.exists(image_save_path):
|
| 161 |
+
pil_image.save(image_save_path)
|
| 162 |
+
|
| 163 |
+
# 4. (Key step) Get Visual Fact
|
| 164 |
+
visual_fact_data = None
|
| 165 |
+
if fetch_visual_facts:
|
| 166 |
+
# Use API ports in a round-robin manner according to index 'i'
|
| 167 |
+
port_to_use = api_ports_list[i % len(api_ports_list)]
|
| 168 |
+
api_url = API_URL_TEMPLATE.format(port=port_to_use)
|
| 169 |
+
|
| 170 |
+
visual_fact_data = get_visual_fact(api_url, pil_image, question)
|
| 171 |
+
|
| 172 |
+
# 5. Build metadata dict (using the new format you specified)
|
| 173 |
+
metadata = {
|
| 174 |
+
"question": question,
|
| 175 |
+
"question_wo_prompt": question,
|
| 176 |
+
"answer": answer,
|
| 177 |
+
"choices": choices_list, # Store the full options list
|
| 178 |
+
"image": image_save_path,
|
| 179 |
+
"visual_fact": visual_fact_data,
|
| 180 |
+
"hint": hint
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
return metadata
|
| 184 |
+
|
| 185 |
+
except Exception as e:
|
| 186 |
+
print(f"!! Fatal error in worker (index {i}): {e}")
|
| 187 |
+
return None # The main process will filter out None
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def save_aokvqa_with_facts(base_output_dir=None, fetch_visual_facts=None):
|
| 191 |
+
"""
|
| 192 |
+
Load A-OKVQA, save images with multiprocessing, and optionally obtain visual_facts for the training split.
|
| 193 |
+
|
| 194 |
+
Set env FETCH_VISUAL_FACTS=1 to call local MLLM APIs; default is images-only download.
|
| 195 |
+
"""
|
| 196 |
+
if base_output_dir is None:
|
| 197 |
+
base_output_dir = AOKVQA_DIR
|
| 198 |
+
if fetch_visual_facts is None:
|
| 199 |
+
fetch_visual_facts = os.environ.get("FETCH_VISUAL_FACTS", "0") == "1"
|
| 200 |
+
|
| 201 |
+
# 1. Define output directories
|
| 202 |
+
base_dir_abs = os.path.abspath(base_output_dir)
|
| 203 |
+
image_output_dir = os.path.join(base_dir_abs, "images")
|
| 204 |
+
json_output_dir = os.path.join(base_dir_abs, "json")
|
| 205 |
+
|
| 206 |
+
# 2. Create directories
|
| 207 |
+
os.makedirs(image_output_dir, exist_ok=True)
|
| 208 |
+
os.makedirs(json_output_dir, exist_ok=True)
|
| 209 |
+
|
| 210 |
+
print(f"Images will be saved to (absolute path): {image_output_dir}")
|
| 211 |
+
print(f"JSON will be saved to (absolute path): {json_output_dir}")
|
| 212 |
+
print(f"Using {len(API_PORTS)} API ports: {API_PORTS}")
|
| 213 |
+
|
| 214 |
+
# 3. Load dataset
|
| 215 |
+
print("Loading HuggingFaceM4/A-OKVQA dataset...")
|
| 216 |
+
try:
|
| 217 |
+
dataset = load_dataset("HuggingFaceM4/A-OKVQA")
|
| 218 |
+
# Use a small subset for testing
|
| 219 |
+
# dataset['train'] = dataset['train'].select(range(100))
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Failed to load dataset: {e}")
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
print(f"Dataset splits: {list(dataset.keys())}")
|
| 225 |
+
|
| 226 |
+
# 4. Define number of worker processes
|
| 227 |
+
num_workers = 64
|
| 228 |
+
print(f"Starting {num_workers} worker processes...")
|
| 229 |
+
|
| 230 |
+
# 5. Iterate over each split
|
| 231 |
+
for split in dataset.keys():
|
| 232 |
+
|
| 233 |
+
fetch_facts = fetch_visual_facts and (split == 'train')
|
| 234 |
+
|
| 235 |
+
if fetch_facts:
|
| 236 |
+
print(f"\n--- Processing split {split} (will call MLLM API) ---")
|
| 237 |
+
else:
|
| 238 |
+
print(f"\n--- Processing split {split} (images only, visual_fact=None) ---")
|
| 239 |
+
|
| 240 |
+
metadata_list = []
|
| 241 |
+
|
| 242 |
+
worker_func = partial(
|
| 243 |
+
process_example_worker,
|
| 244 |
+
split=split,
|
| 245 |
+
image_output_dir=image_output_dir,
|
| 246 |
+
api_ports_list=API_PORTS,
|
| 247 |
+
fetch_visual_facts=fetch_facts
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
tasks = list(enumerate(dataset[split]))
|
| 251 |
+
total_count = len(tasks)
|
| 252 |
+
|
| 253 |
+
# 6. Use multiprocessing pool
|
| 254 |
+
with multiprocessing.Pool(processes=num_workers) as pool:
|
| 255 |
+
for result in tqdm(pool.imap_unordered(worker_func, tasks), total=total_count, desc=f"Processing {split}"):
|
| 256 |
+
if result:
|
| 257 |
+
metadata_list.append(result)
|
| 258 |
+
|
| 259 |
+
print(f" Split {split} finished. Success: {len(metadata_list)} / {total_count}.")
|
| 260 |
+
|
| 261 |
+
# 7. (Optional) Sort
|
| 262 |
+
metadata_list.sort(key=lambda x: x['image'])
|
| 263 |
+
|
| 264 |
+
# 8. Write JSON file
|
| 265 |
+
json_filename = os.path.join(json_output_dir, f"{split}.json")
|
| 266 |
+
print(f"Saving {len(metadata_list)} metadata entries to {json_filename}...")
|
| 267 |
+
|
| 268 |
+
with open(json_filename, 'w', encoding='utf-8') as f:
|
| 269 |
+
json.dump(metadata_list, f, indent=4, ensure_ascii=False)
|
| 270 |
+
|
| 271 |
+
print(f"\n--- All processing completed! ---")
|
| 272 |
+
print(f"All image files saved in: '{image_output_dir}'")
|
| 273 |
+
print(f"All JSON files saved in: '{json_output_dir}'")
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
if __name__ == "__main__":
|
| 277 |
+
multiprocessing.set_start_method("spawn", force=True)
|
| 278 |
+
|
| 279 |
+
save_aokvqa_with_facts()
|
data_utils/aokvqa/evaluator.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- New: A-OKVQA evaluation function ---
|
| 2 |
+
def eval_aokvqa_direct(prediction, ground_truth_list):
|
| 3 |
+
"""
|
| 4 |
+
Simple VQA accuracy: returns 1 if the prediction is in the direct_answers list, otherwise 0.
|
| 5 |
+
Matching is case-insensitive.
|
| 6 |
+
"""
|
| 7 |
+
if not ground_truth_list: # Handle empty ground truth list
|
| 8 |
+
return 0.0
|
| 9 |
+
|
| 10 |
+
# Clean predicted answer
|
| 11 |
+
pred_cleaned = prediction.lower().strip().strip('.').strip()
|
| 12 |
+
|
| 13 |
+
# Create a cleaned set of ground truth answers
|
| 14 |
+
gt_cleaned_set = set(ans.lower().strip().strip('.').strip() for ans in ground_truth_list)
|
| 15 |
+
|
| 16 |
+
# Check if prediction is in the set
|
| 17 |
+
if pred_cleaned in gt_cleaned_set:
|
| 18 |
+
return 1.0
|
| 19 |
+
|
| 20 |
+
# (Optional) You can add more sophisticated VQA matching logic, but simple "in" check is the baseline
|
| 21 |
+
return 0.0
|
data_utils/aokvqa/patch_aokvqa.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from datasets import load_dataset
|
| 4 |
+
from tqdm import tqdm
|
| 5 |
+
import shutil
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def patch_json_with_direct_answers(base_output_dir):
|
| 9 |
+
"""
|
| 10 |
+
Load existing JSON files and the original A-OKVQA dataset,
|
| 11 |
+
and add the 'direct_answers' field back into the JSON files.
|
| 12 |
+
|
| 13 |
+
This script assumes that the entries in your generated JSON files
|
| 14 |
+
(e.g., train.json) are ordered according to the original dataset indices.
|
| 15 |
+
Your original script's
|
| 16 |
+
`metadata_list.sort(key=lambda x: x['image'])` ensures this alignment.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
json_output_dir = os.path.join(base_output_dir, "json")
|
| 20 |
+
print(f"JSON directory to patch: {json_output_dir}")
|
| 21 |
+
|
| 22 |
+
# 1. Load the original dataset
|
| 23 |
+
print("Loading HuggingFaceM4/A-OKVQA dataset (metadata only)...")
|
| 24 |
+
try:
|
| 25 |
+
# Specify a cache directory to avoid repeated downloads
|
| 26 |
+
cache_dir = os.path.join(base_output_dir, ".cache")
|
| 27 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 28 |
+
dataset = load_dataset("HuggingFaceM4/A-OKVQA", cache_dir=cache_dir)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"Failed to load original dataset: {e}")
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
print(f"Available splits in dataset: {list(dataset.keys())}")
|
| 34 |
+
|
| 35 |
+
# 2. Iterate over each split
|
| 36 |
+
for split in dataset.keys():
|
| 37 |
+
json_filename = os.path.join(json_output_dir, f"{split}.json")
|
| 38 |
+
|
| 39 |
+
if not os.path.exists(json_filename):
|
| 40 |
+
print(f"!! Warning: {json_filename} not found, skipping split '{split}'.")
|
| 41 |
+
continue
|
| 42 |
+
|
| 43 |
+
print(f"\n--- Patching split {split} ({json_filename}) ---")
|
| 44 |
+
|
| 45 |
+
# 3. Load existing (incomplete) JSON data
|
| 46 |
+
try:
|
| 47 |
+
with open(json_filename, 'r', encoding='utf-8') as f:
|
| 48 |
+
generated_data_list = json.load(f)
|
| 49 |
+
print(f" Loaded {len(generated_data_list)} processed entries.")
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f" !! Error: Failed to load {json_filename}: {e}")
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
# 4. Load original split data
|
| 55 |
+
original_split_data = dataset[split]
|
| 56 |
+
print(f" Loaded {len(original_split_data)} original entries.")
|
| 57 |
+
|
| 58 |
+
# 5. Sanity check (ensure counts match)
|
| 59 |
+
if len(generated_data_list) != len(original_split_data):
|
| 60 |
+
print(f" !! Critical error: Mismatch in number of entries!")
|
| 61 |
+
print(f" JSON ({split}.json) has {len(generated_data_list)} records.")
|
| 62 |
+
print(f" Original dataset ('{split}') has {len(original_split_data)} records.")
|
| 63 |
+
print(f" Skipping this split.")
|
| 64 |
+
continue
|
| 65 |
+
|
| 66 |
+
# 6. Core logic: merge data using zip (relying on aligned ordering)
|
| 67 |
+
# Your 'image' field (e.g., "train_0000001.png") ensures that
|
| 68 |
+
# the order of 'generated_data_list' matches 'original_split_data'.
|
| 69 |
+
|
| 70 |
+
print(f" Merging 'direct_answers'...")
|
| 71 |
+
for generated_metadata, original_example in \
|
| 72 |
+
tqdm(zip(generated_data_list, original_split_data),
|
| 73 |
+
total=len(generated_data_list),
|
| 74 |
+
desc=f"Merging {split}"):
|
| 75 |
+
# Add (or overwrite) the missing field
|
| 76 |
+
generated_metadata['direct_answers'] = original_example.get('direct_answers')
|
| 77 |
+
|
| 78 |
+
# 7. Backup and overwrite
|
| 79 |
+
backup_filename = os.path.join(json_output_dir, f"{split}.backup.json")
|
| 80 |
+
try:
|
| 81 |
+
if not os.path.exists(backup_filename): # Backup only once
|
| 82 |
+
shutil.copyfile(json_filename, backup_filename)
|
| 83 |
+
print(f" Backed up original file to {backup_filename}")
|
| 84 |
+
else:
|
| 85 |
+
print(f" Backup file {backup_filename} already exists, will overwrite {json_filename} directly")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f" !! Warning: Failed to create backup: {e}. Will overwrite directly.")
|
| 88 |
+
|
| 89 |
+
print(f" Writing {len(generated_data_list)} updated metadata entries back to {json_filename}...")
|
| 90 |
+
with open(json_filename, 'w', encoding='utf-8') as f:
|
| 91 |
+
# generated_data_list has already been modified in memory
|
| 92 |
+
json.dump(generated_data_list, f, indent=4, ensure_ascii=False)
|
| 93 |
+
|
| 94 |
+
print("\n--- Patching completed! ---")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
from data_utils.paths import AOKVQA_DIR
|
| 99 |
+
|
| 100 |
+
print(f"Target root directory: {AOKVQA_DIR}")
|
| 101 |
+
patch_json_with_direct_answers(base_output_dir=AOKVQA_DIR)
|
data_utils/chart/__pycache__/evaluator.cpython-310.pyc
ADDED
|
Binary file (8.35 kB). View file
|
|
|
data_utils/chart/pre_refinement.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# process_json_distributed.py
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import multiprocessing
|
| 5 |
+
import os
|
| 6 |
+
from copy import deepcopy
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
from data_utils.paths import CHARTQA_JSON_DIR, PROJECT_ROOT
|
| 11 |
+
sys.path.append(PROJECT_ROOT)
|
| 12 |
+
from client_utils.openai_api import OpenAIClient
|
| 13 |
+
from data_utils.chart.prompts import prompt_refine
|
| 14 |
+
from data_utils.commom_util import prompt_ic
|
| 15 |
+
|
| 16 |
+
class ContextRefiner:
|
| 17 |
+
|
| 18 |
+
def __init__(self, CLIENT_CONFIG, gpu_id=0):
|
| 19 |
+
self.refine_templetes = ["""Goal: [State the user's objective, e.g., Find the year with the highest sales]
|
| 20 |
+
Observation: [List key data points from the chart, e.g., 2020: 150, 2021: 200, 2022: 180]
|
| 21 |
+
Reasoning: [State the logical step, e.g., Compare the values. 200 is the maximum.]
|
| 22 |
+
Conclusion: [Draw the conclusion, e.g., The year with the highest sales was 2021.]
|
| 23 |
+
"""]
|
| 24 |
+
if CLIENT_CONFIG['client_type'] == 'openai':
|
| 25 |
+
|
| 26 |
+
self.client = OpenAIClient(config=CLIENT_CONFIG)
|
| 27 |
+
else:
|
| 28 |
+
raise ValueError(f"Client type '{CLIENT_CONFIG['client_type']}' not supported.")
|
| 29 |
+
|
| 30 |
+
def refine_hint(self, question: str, hint: str, reference_answer: str, task: str):
|
| 31 |
+
if not hint:
|
| 32 |
+
return hint
|
| 33 |
+
system_prompt = None
|
| 34 |
+
if 'chart' in task:
|
| 35 |
+
system_prompt = 'You are a seasoned professional in the field of chart analysis...'
|
| 36 |
+
else:
|
| 37 |
+
raise Exception('Unknown expert task')
|
| 38 |
+
try:
|
| 39 |
+
in_context_example = self.client.get_completion(prompt_ic % hint, system_prompt=system_prompt,
|
| 40 |
+
max_tokens=5000)
|
| 41 |
+
if 'chart' in task:
|
| 42 |
+
evaluation_prompt = prompt_refine % (in_context_example, question, reference_answer,
|
| 43 |
+
self.refine_templetes[0])
|
| 44 |
+
output = self.client.get_completion(evaluation_prompt, system_prompt=system_prompt, max_tokens=1000)
|
| 45 |
+
return output
|
| 46 |
+
else:
|
| 47 |
+
raise ValueError(f"Task '{task}' not supported for thinking reward.")
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"Error occurred while processing '{question}': {e}")
|
| 50 |
+
return hint
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
refiner_instance = None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def worker_initializer(base_client_config):
|
| 57 |
+
global refiner_instance
|
| 58 |
+
|
| 59 |
+
# Key change: get the unique ID of the current worker process (starting from 1)
|
| 60 |
+
# This is the variable we use to simulate gpu_id
|
| 61 |
+
worker_id = multiprocessing.current_process()._identity[0] - 1
|
| 62 |
+
|
| 63 |
+
# Create a deep copy of the config to avoid interference between processes
|
| 64 |
+
worker_config = deepcopy(base_client_config)
|
| 65 |
+
|
| 66 |
+
# Key change: implement your port calculation logic
|
| 67 |
+
if worker_config.get('init_port') is not None and worker_config.get('num_server') is not None:
|
| 68 |
+
num_server = int(worker_config['num_server'])
|
| 69 |
+
# server_id decides which port to use
|
| 70 |
+
server_id = worker_id % num_server
|
| 71 |
+
port = worker_config['init_port'] + server_id
|
| 72 |
+
|
| 73 |
+
# Format api_base to assign a fixed port for this process
|
| 74 |
+
worker_config['api_base'] = worker_config['api_base'] % str(port)
|
| 75 |
+
|
| 76 |
+
print(f"Process {os.getpid()} (Worker-{worker_id}) initializing... connecting to {worker_config['api_base']}")
|
| 77 |
+
else:
|
| 78 |
+
print(f"Process {os.getpid()} (Worker-{worker_id}) initializing... using default api_base")
|
| 79 |
+
|
| 80 |
+
# Use the customized config for this specific process to create the instance
|
| 81 |
+
refiner_instance = ContextRefiner(worker_config, gpu_id=worker_id)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def process_item_worker(item):
|
| 85 |
+
"""Function executed by a single worker process (unchanged)"""
|
| 86 |
+
global refiner_instance
|
| 87 |
+
if refiner_instance is None:
|
| 88 |
+
raise Exception("Refiner has not been initialized in the worker process!")
|
| 89 |
+
|
| 90 |
+
new_hint = refiner_instance.refine_hint(
|
| 91 |
+
question=item['question'],
|
| 92 |
+
hint=item['hint'],
|
| 93 |
+
reference_answer=item['answer'],
|
| 94 |
+
task='chart'
|
| 95 |
+
)
|
| 96 |
+
item['hint'] = new_hint
|
| 97 |
+
return item
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ---------------- Main logic ----------------
|
| 101 |
+
def main():
|
| 102 |
+
# Configuration that contains port and server count information
|
| 103 |
+
from config import CLIENT_CONFIG
|
| 104 |
+
input_filename = os.path.join(CHARTQA_JSON_DIR, 'train.json')
|
| 105 |
+
output_filename = os.path.join(CHARTQA_JSON_DIR, 'train_new_prerefine.json')
|
| 106 |
+
|
| 107 |
+
NUM_PROCESSES = 64
|
| 108 |
+
print(f"Using {NUM_PROCESSES} processes and distributing requests to {CLIENT_CONFIG['num_server']} servers...")
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
with open(input_filename, 'r', encoding='utf-8') as f:
|
| 112 |
+
data = json.load(f)
|
| 113 |
+
except FileNotFoundError:
|
| 114 |
+
print(f"Error: input file '{input_filename}' not found.")
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
processed_data = []
|
| 118 |
+
|
| 119 |
+
# Key point: pass the base configuration to the initializer of each process
|
| 120 |
+
with multiprocessing.Pool(processes=NUM_PROCESSES, initializer=worker_initializer,
|
| 121 |
+
initargs=(CLIENT_CONFIG,)) as pool:
|
| 122 |
+
with tqdm(total=len(data), desc="Processing JSON in parallel") as pbar:
|
| 123 |
+
for result in pool.imap_unordered(process_item_worker, data):
|
| 124 |
+
processed_data.append(result)
|
| 125 |
+
pbar.update(1)
|
| 126 |
+
|
| 127 |
+
with open(output_filename, 'w', encoding='utf-8') as f:
|
| 128 |
+
json.dump(processed_data, f, ensure_ascii=False, indent=4)
|
| 129 |
+
|
| 130 |
+
print(f"\nProcessing completed! Results saved to '{output_filename}'.")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
main()
|
data_utils/chart/prompts.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
prompt_thinking_reward = """
|
| 2 |
+
Given
|
| 3 |
+
<IC>: the data of an image
|
| 4 |
+
<Q>: a question
|
| 5 |
+
<A>: a reference answer
|
| 6 |
+
<R>: a reasoning text
|
| 7 |
+
|
| 8 |
+
Goal:
|
| 9 |
+
Assess whether <R> correctly and reasonably uses visible data in <IC> to justify that the correct answer to <Q> is <A>. Rate the quality as low / medium / high according to:
|
| 10 |
+
(a) low: Does not use data from <IC> at all, or the language is not fluent/natural, or it fails to indicate the answer to <Q> is <A>, or the structure is not good.
|
| 11 |
+
(b) medium: Uses data from <IC> and is written fluently, but the reasoning is overly brief or insufficiently clear.
|
| 12 |
+
(c) high: Uses data from <IC> and is written fluently; Has a very strong CoT structure (e.g. clear and logical steps); the reasoning progresses step by step with depth, each step is correct and reasonable; the data from <IC> appears exactly where it should; overall, the reasoning text provides very strong support that the answer to <Q> is <A>.
|
| 13 |
+
|
| 14 |
+
Example:
|
| 15 |
+
<IC>: [
|
| 16 |
+
{"object": "bar", "attributes": ["~120k", "Q4"], "label": "Product A"},
|
| 17 |
+
{"object": "bar", "attributes": ["~150k", "Q4"], "label": "Product B"},
|
| 18 |
+
{"object": "bar", "attributes": ["~90k", "Q4"], "label": "Product C"},
|
| 19 |
+
{"title": "Quarterly Revenue"}
|
| 20 |
+
]
|
| 21 |
+
<Q>: Which product has the highest revenue in Q4?
|
| 22 |
+
<A>: product b
|
| 23 |
+
<R>:
|
| 24 |
+
[Extraction] Reads Q4 bar heights: A ~120k, B ~150k, C ~90k.
|
| 25 |
+
[Calculation] Compares values: B > A and B > C.
|
| 26 |
+
[Conclusion] Therefore, Product B is highest, matching the answer "product b".
|
| 27 |
+
|
| 28 |
+
<Output>: medium
|
| 29 |
+
|
| 30 |
+
According to the requirements and examples above, score the input into three categories. Please give me the result directly without any explanation or description.
|
| 31 |
+
|
| 32 |
+
<IC>: %s
|
| 33 |
+
<Q>: %s
|
| 34 |
+
<A>: %s
|
| 35 |
+
<R>: %s
|
| 36 |
+
<Output>:
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
prompt_refine = """Given:
|
| 41 |
+
<IC>: the data of an image
|
| 42 |
+
<Q>: a question
|
| 43 |
+
<A>: a reference answer
|
| 44 |
+
<T>: a writing template
|
| 45 |
+
|
| 46 |
+
Goal:
|
| 47 |
+
Transform the visual information in <IC> into a textualized data description and incorporate it into a smooth, natural explanation that reasons why the correct answer to <Q> is <A>, using the format and tone defined by <T>.
|
| 48 |
+
|
| 49 |
+
Example:
|
| 50 |
+
<IC>: [
|
| 51 |
+
{"Year": 2011, "Favorable": 0, "Unfavorable": 3.1},
|
| 52 |
+
{"Year": 2012, "Favorable": 56, "Unfavorable": 38.0},
|
| 53 |
+
{"Year": 2013, "Favorable": 0, "Unfavorable": 0.0},
|
| 54 |
+
{"Year": 2014, "Favorable": 51, "Unfavorable": 48.0},
|
| 55 |
+
{"Year": 2015, "Favorable": 0, "Unfavorable": 53.0}
|
| 56 |
+
]
|
| 57 |
+
<Q>: In which year the value was 51?
|
| 58 |
+
<A>: 2014
|
| 59 |
+
<T>:
|
| 60 |
+
Goal: [State the user's objective, e.g., Find the year with the highest sales]
|
| 61 |
+
Observation: [List key data points from the chart, e.g., 2020: 150, 2021: 200, 2022: 180]
|
| 62 |
+
Reasoning: [State the logical step, e.g., Compare the values. 200 is the maximum.]
|
| 63 |
+
Conclusion: [Draw the conclusion, e.g., The year with the highest sales was 2021.]
|
| 64 |
+
|
| 65 |
+
<Output>:
|
| 66 |
+
Goal: Find the year in which the 'Favorable' value was 51.
|
| 67 |
+
Observation: The data shows the 'Favorable' values for each year are: 2011: 0, 2012: 56, 2013: 0, 2014: 51, and 2015: 0.
|
| 68 |
+
Reasoning: Scanning the 'Favorable' column for the number 51 leads to the corresponding year in that row.
|
| 69 |
+
Conclusion: The value 51 occurred in the year 2014.
|
| 70 |
+
|
| 71 |
+
Now, according to the requirements and the examples above, convert my input into the target reasoning text. Please give me the result directly without any explanation or description.
|
| 72 |
+
|
| 73 |
+
<IC>: %s
|
| 74 |
+
<Q>: %s
|
| 75 |
+
<A>: %s
|
| 76 |
+
<T>: %s
|
| 77 |
+
<Output>:
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
prompt_template = """Analyze the preceding text (which is a Chain of Thought, or "CoT").
|
| 81 |
+
|
| 82 |
+
**Your Task:**
|
| 83 |
+
1. **Evaluate Structure and Logic:** First, determine if the CoT is **well-structured** and **logical**. A "well-structured" CoT contains clear, distinct, and labeled reasoning steps (e.g., "Goal:", "Observation:", "Reasoning:", "Conclusion:").
|
| 84 |
+
2. **Extract Template:** If (and only if) the CoT is **well-structured**, extract a **generic reasoning template** from it. In this template, use brackets `[ ]` to describe the general purpose of each step (as shown in Example 1).
|
| 85 |
+
3. **Output None:** If the CoT is **unstructured** (e.g., it reads like a single conversational paragraph), lacks clear steps, or is logically unsound, you must output **None** (as shown in Example 2).
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
**Example 1: [Well-Structured CoT]**
|
| 90 |
+
|
| 91 |
+
**Input:**
|
| 92 |
+
"Goal: Find the lowest value of the red graph.\nObservation: The data for the 'Rep/Lean Rep' category across the years are: 2018: 72, 2019: 70, and 2020: 77.\nReasoning: Comparing the values, the minimum value is 70.\nConclusion: The lowest value of the red graph is 70."
|
| 93 |
+
|
| 94 |
+
**Output:**
|
| 95 |
+
Goal: [State the user's objective, e.g., Find the year with the highest sales]
|
| 96 |
+
Observation: [List key data points from the chart, e.g., 2020: 150, 2021: 200, 2022: 180]
|
| 97 |
+
Reasoning: [State the logical step, e.g., Compare the values. 200 is the maximum.]
|
| 98 |
+
Conclusion: [Draw the conclusion, e.g., The year with the highest sales was 2021.]
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
**Example 2: [Poorly-Structured / Conversational CoT]**
|
| 103 |
+
|
| 104 |
+
**Input:**
|
| 105 |
+
I'm trying to figure out the year when the unfavorable view reaches its highest point. Looking at the data, I see that the values for each year are pretty low until 2016, where it jumps to 55. But the peak doesn't happen until 2017, when the value spikes to 64. So, it seems like the unfavorable view really hits its maximum in 2017.
|
| 106 |
+
|
| 107 |
+
**Output:**
|
| 108 |
+
None
|
| 109 |
+
|
| 110 |
+
Now, according to the requirements and the examples above, give me the result directly without any explanation or description.
|
| 111 |
+
**Input:**
|
| 112 |
+
%s
|
| 113 |
+
**Output:**
|
| 114 |
+
"""
|
data_utils/clevr/clevr_processor.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
from accelerate.utils import gather_object
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
from transformers import activations
|
| 7 |
+
|
| 8 |
+
activations.PytorchGELUTanh = activations.GELUTanh
|
| 9 |
+
except ImportError:
|
| 10 |
+
print("Note: Unable to apply PytorchGELUTanh patch. If you encounter an ImportError, please check the transformers version.")
|
| 11 |
+
# --- End of patch ---
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import shutil
|
| 15 |
+
import json
|
| 16 |
+
import re
|
| 17 |
+
import numpy as np
|
| 18 |
+
from PIL import Image
|
| 19 |
+
from tqdm import tqdm
|
| 20 |
+
from datasets import load_dataset
|
| 21 |
+
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
|
| 22 |
+
from accelerate import Accelerator
|
| 23 |
+
|
| 24 |
+
# (qwen_vl_utils import and fallback remain unchanged)
|
| 25 |
+
try:
|
| 26 |
+
from qwen_vl_utils import process_vision_info
|
| 27 |
+
except ImportError:
|
| 28 |
+
print("Warning: Failed to import 'qwen_vl_utils.process_vision_info'.")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def process_vision_info(messages):
|
| 32 |
+
images = []
|
| 33 |
+
for msg in messages:
|
| 34 |
+
if msg['role'] == 'user':
|
| 35 |
+
for content in msg['content']:
|
| 36 |
+
if content['type'] == 'image':
|
| 37 |
+
images.append(content['image'])
|
| 38 |
+
return images, None # Return (images, videos)
|
| 39 |
+
|
| 40 |
+
Image.MAX_IMAGE_PIXELS = None
|
| 41 |
+
|
| 42 |
+
# (RexOmni dependency and DummyRex remain unchanged)
|
| 43 |
+
try:
|
| 44 |
+
from rex_omni import RexOmniWrapper
|
| 45 |
+
except ImportError:
|
| 46 |
+
print("Warning: 'from rex_omni import RexOmniWrapper' failed.")
|
| 47 |
+
print("Using a dummy RexOmniWrapper (DummyRex) for testing only.")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class DummyRex:
|
| 51 |
+
def __init__(self, *args, **kwargs):
|
| 52 |
+
print("INFO: DUMMY: Using DummyRex detector.")
|
| 53 |
+
|
| 54 |
+
def inference(self, images, task, categories, **kwargs):
|
| 55 |
+
print("INFO: DUMMY: DummyRex returning fake center boxes.")
|
| 56 |
+
|
| 57 |
+
# Batch-supporting Dummy
|
| 58 |
+
results = []
|
| 59 |
+
|
| 60 |
+
# Ensure images is a list
|
| 61 |
+
if not isinstance(images, list):
|
| 62 |
+
images = [images]
|
| 63 |
+
|
| 64 |
+
for img in images:
|
| 65 |
+
if isinstance(img, Image.Image):
|
| 66 |
+
w, h = img.size
|
| 67 |
+
else:
|
| 68 |
+
w, h = 800, 600
|
| 69 |
+
x0, y0 = w * 0.25, h * 0.25
|
| 70 |
+
x1, y1 = w * 0.75, h * 0.75
|
| 71 |
+
results.append({"extracted_predictions": {"anything": [{"type": "box", "coords": [x0, y0, x1, y1]}]}})
|
| 72 |
+
return results
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
RexOmniWrapper = DummyRex
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _strip_tags(text, tag_name):
|
| 79 |
+
# (unchanged)
|
| 80 |
+
if not isinstance(text, str):
|
| 81 |
+
text = str(text)
|
| 82 |
+
text = re.sub(rf'<{tag_name}>', '', text, flags=re.IGNORECASE)
|
| 83 |
+
text = re.sub(rf'</{tag_name}>', '', text, flags=re.IGNORECASE)
|
| 84 |
+
return text.strip()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# --- Core VQA helper functions (moved to global scope) ---
|
| 88 |
+
|
| 89 |
+
def _crop_and_expand_box(image, box, padding_pixels=20):
|
| 90 |
+
# (unchanged)
|
| 91 |
+
x0, y0, x1, y1 = [int(c) for c in box]
|
| 92 |
+
img_w, img_h = image.size
|
| 93 |
+
x0_new = max(0, x0 - padding_pixels)
|
| 94 |
+
y0_new = max(0, y0 - padding_pixels)
|
| 95 |
+
x1_new = min(img_w, x1 + padding_pixels)
|
| 96 |
+
y1_new = min(img_h, y1 + padding_pixels)
|
| 97 |
+
return image.crop((x0_new, y0_new, x1_new, y1_new))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# --- ★★★ Optimization 1: Change VQA queries to batched processing ★★★ ---
|
| 101 |
+
def _query_qwen_vl_BATCH(crop_images_list, model, processor, accelerator):
|
| 102 |
+
"""
|
| 103 |
+
Use Qwen-VL to query cropped image patches in batch and return a list of JSON strings.
|
| 104 |
+
"""
|
| 105 |
+
if not crop_images_list:
|
| 106 |
+
return []
|
| 107 |
+
|
| 108 |
+
prompt = """This is an object from a CLEVR scene. Analyze the primary object in the image.
|
| 109 |
+
Respond *strictly* with a JSON list (containing one dictionary) in the following format:
|
| 110 |
+
[
|
| 111 |
+
{"object": "object_name", "attributes": ["attr1", "attr2"]}
|
| 112 |
+
]
|
| 113 |
+
- "object": The shape of the object (e.g., "sphere", "cube", "cylinder", "cone").
|
| 114 |
+
- "attributes": A list of visual attributes (e.g., ["blue", "large", "metal", "shiny", "rubber"]).
|
| 115 |
+
Provide only the JSON list:"""
|
| 116 |
+
|
| 117 |
+
# 1. Create messages for each image in the batch
|
| 118 |
+
template_messages = [
|
| 119 |
+
{
|
| 120 |
+
"role": "user",
|
| 121 |
+
"content": [
|
| 122 |
+
{"type": "image", "image": "placeholder.jpg"}, # Placeholder
|
| 123 |
+
{"type": "text", "text": prompt},
|
| 124 |
+
],
|
| 125 |
+
}
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
# 2. Generate chat prompt text once
|
| 130 |
+
chat_prompt_text = processor.apply_chat_template(
|
| 131 |
+
template_messages, tokenize=False, add_generation_prompt=True
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
num_crops = len(crop_images_list)
|
| 135 |
+
batch_text = [chat_prompt_text] * num_crops
|
| 136 |
+
batch_images = crop_images_list
|
| 137 |
+
|
| 138 |
+
unwrapped_model = accelerator.unwrap_model(model)
|
| 139 |
+
|
| 140 |
+
# 3. Use text list and image list for batched processing
|
| 141 |
+
inputs = processor(
|
| 142 |
+
text=batch_text,
|
| 143 |
+
images=batch_images,
|
| 144 |
+
padding=True, # Important: enable padding to handle batching
|
| 145 |
+
return_tensors="pt",
|
| 146 |
+
).to(unwrapped_model.device)
|
| 147 |
+
|
| 148 |
+
# 4. Batch generation
|
| 149 |
+
generated_ids = unwrapped_model.generate(**inputs, max_new_tokens=256, do_sample=False)
|
| 150 |
+
|
| 151 |
+
generated_ids_trimmed = [
|
| 152 |
+
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
# 5. Batch decode
|
| 156 |
+
output_texts_list = processor.batch_decode(
|
| 157 |
+
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
return output_texts_list
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
print(f"Qwen-VL batched inference failed: {e}")
|
| 164 |
+
# Return a list of empty JSON placeholders to avoid zip errors
|
| 165 |
+
return ["[]"] * len(crop_images_list)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _parse_qwen_json(response_text):
|
| 169 |
+
# (unchanged)
|
| 170 |
+
try:
|
| 171 |
+
match = re.search(r'```json\s*(\[.*\])\s*```', response_text, re.DOTALL)
|
| 172 |
+
if match:
|
| 173 |
+
json_str = match.group(1)
|
| 174 |
+
return json.loads(json_str)
|
| 175 |
+
match = re.search(r'(\[.*\])', response_text, re.DOTALL)
|
| 176 |
+
if match:
|
| 177 |
+
json_str = match.group(1)
|
| 178 |
+
return json.loads(json_str)
|
| 179 |
+
return []
|
| 180 |
+
except json.JSONDecodeError:
|
| 181 |
+
print(f"Failed to parse JSON: {response_text}")
|
| 182 |
+
return []
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print(f"Unknown error occurred while parsing JSON: {e}")
|
| 185 |
+
return []
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _load_and_preprocess_data(base_output_dir, image_output_dir):
|
| 189 |
+
# (unchanged)
|
| 190 |
+
print("Loading 'MMInstruction/Clevr_CoGenT_TrainA_R1'...")
|
| 191 |
+
try:
|
| 192 |
+
dataset = load_dataset("MMInstruction/Clevr_CoGenT_TrainA_R1", split='train')
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f"Failed to load dataset 'MMInstruction/Clevr_CoGenT_TrainA_R1': {e}")
|
| 195 |
+
return []
|
| 196 |
+
|
| 197 |
+
# Only use the first few samples for testing (still 100 here)
|
| 198 |
+
# dataset = dataset.select(range(100))
|
| 199 |
+
print(f"Loaded {len(dataset)} samples.")
|
| 200 |
+
|
| 201 |
+
job_list = []
|
| 202 |
+
print("Preprocessing data (saving images and parsing text)...")
|
| 203 |
+
for i, example in enumerate(tqdm(dataset, desc="Preprocessing progress")):
|
| 204 |
+
prompt = example['problem']
|
| 205 |
+
hint = _strip_tags(example['thinking'], 'think')
|
| 206 |
+
answer = _strip_tags(example['solution'], 'answer')
|
| 207 |
+
|
| 208 |
+
image = example['image']
|
| 209 |
+
if not isinstance(image, Image.Image):
|
| 210 |
+
print(f"Warning: sample {i} is not a PIL image, skipped.")
|
| 211 |
+
continue
|
| 212 |
+
|
| 213 |
+
image_filename = f"clevr_cogent_trainA_r1_{i:07d}.jpg"
|
| 214 |
+
destination_image_path = os.path.join(image_output_dir, image_filename)
|
| 215 |
+
|
| 216 |
+
try:
|
| 217 |
+
os.makedirs(os.path.dirname(destination_image_path), exist_ok=True)
|
| 218 |
+
if not os.path.exists(destination_image_path):
|
| 219 |
+
image.convert("RGB").save(destination_image_path, "JPEG")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"Warning: failed to save image for sample {i}, skipped. Error: {e}")
|
| 222 |
+
continue
|
| 223 |
+
|
| 224 |
+
job_list.append({
|
| 225 |
+
"prompt": prompt,
|
| 226 |
+
"answer": answer,
|
| 227 |
+
"hint": hint,
|
| 228 |
+
"destination_image_path": destination_image_path
|
| 229 |
+
})
|
| 230 |
+
|
| 231 |
+
print(f"Successfully preprocessed {len(job_list)} items.")
|
| 232 |
+
|
| 233 |
+
job_list_path = os.path.join(base_output_dir, "job_list.json")
|
| 234 |
+
with open(job_list_path, 'w', encoding='utf-8') as f:
|
| 235 |
+
json.dump(job_list, f)
|
| 236 |
+
|
| 237 |
+
print(f"Job list saved to: {job_list_path}")
|
| 238 |
+
return job_list
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def main():
|
| 242 |
+
# (1. Initialize Accelerator - unchanged)
|
| 243 |
+
accelerator = Accelerator()
|
| 244 |
+
|
| 245 |
+
# (0. Define configuration - unchanged)
|
| 246 |
+
MODEL_CONFIGS = {
|
| 247 |
+
"rex_path": "IDEA-Research/Rex-Omni",
|
| 248 |
+
"qwen_path": "Qwen/Qwen2.5-VL-32B-Instruct-AWQ"
|
| 249 |
+
}
|
| 250 |
+
OUTPUT_DIR = "/path/to/data/clevr_cogent_output"
|
| 251 |
+
IMAGE_OUTPUT_DIR = os.path.join(OUTPUT_DIR, "images")
|
| 252 |
+
JSON_OUTPUT_DIR = os.path.join(OUTPUT_DIR, "json")
|
| 253 |
+
|
| 254 |
+
os.makedirs(IMAGE_OUTPUT_DIR, exist_ok=True)
|
| 255 |
+
os.makedirs(JSON_OUTPUT_DIR, exist_ok=True)
|
| 256 |
+
|
| 257 |
+
# (2. Preprocessing (run only on main process) - unchanged)
|
| 258 |
+
job_list_path = os.path.join(OUTPUT_DIR, "job_list.json")
|
| 259 |
+
if accelerator.is_main_process:
|
| 260 |
+
print("Main process [Pre-processing]: loading and preprocessing data...")
|
| 261 |
+
_load_and_preprocess_data(OUTPUT_DIR, IMAGE_OUTPUT_DIR)
|
| 262 |
+
|
| 263 |
+
# (3. Synchronization - unchanged)
|
| 264 |
+
accelerator.wait_for_everyone()
|
| 265 |
+
|
| 266 |
+
# (4. Load and distribute jobs - unchanged)
|
| 267 |
+
if not accelerator.is_main_process:
|
| 268 |
+
print(f"Process {accelerator.process_index}: loading job_list.json...")
|
| 269 |
+
try:
|
| 270 |
+
with open(job_list_path, 'r', encoding='utf-8') as f:
|
| 271 |
+
all_jobs = json.load(f)
|
| 272 |
+
except Exception as e:
|
| 273 |
+
print(f"Process {accelerator.process_index} failed to load job_list.json: {e}")
|
| 274 |
+
return
|
| 275 |
+
total_jobs = len(all_jobs)
|
| 276 |
+
num_processes = accelerator.num_processes
|
| 277 |
+
jobs_per_process = total_jobs // num_processes
|
| 278 |
+
start_index = accelerator.process_index * jobs_per_process
|
| 279 |
+
end_index = (accelerator.process_index + 1) * jobs_per_process
|
| 280 |
+
if accelerator.is_last_process:
|
| 281 |
+
end_index = total_jobs
|
| 282 |
+
my_jobs = all_jobs[start_index:end_index]
|
| 283 |
+
print(f"[Process {accelerator.process_index}]:"
|
| 284 |
+
f" assigned {len(my_jobs)} jobs (indices from {start_index} to {end_index}).")
|
| 285 |
+
|
| 286 |
+
# (5. Load models (each process loads its own copy) - unchanged)
|
| 287 |
+
try:
|
| 288 |
+
try:
|
| 289 |
+
from transformers import activations
|
| 290 |
+
activations.PytorchGELUTanh = activations.GELUTanh
|
| 291 |
+
except ImportError:
|
| 292 |
+
pass
|
| 293 |
+
|
| 294 |
+
print(f"[Process {accelerator.process_index}]: loading RexOmni...")
|
| 295 |
+
rex_model = RexOmniWrapper(
|
| 296 |
+
model_path=MODEL_CONFIGS['rex_path'],
|
| 297 |
+
backend="transformers",
|
| 298 |
+
max_tokens=2048,
|
| 299 |
+
temperature=0.0,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
print(f"[Process {accelerator.process_index}]: loading Qwen-VL...")
|
| 303 |
+
qwen_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
| 304 |
+
MODEL_CONFIGS['qwen_path'],
|
| 305 |
+
torch_dtype="float16",
|
| 306 |
+
device_map="cuda",
|
| 307 |
+
attn_implementation="flash_attention_2"
|
| 308 |
+
)
|
| 309 |
+
qwen_processor = AutoProcessor.from_pretrained(MODEL_CONFIGS['qwen_path'])
|
| 310 |
+
|
| 311 |
+
# Note: RexOmniWrapper (if not Dummy) may need .to(accelerator.device),
|
| 312 |
+
# but Qwen-VL already has its device specified via device_map="cuda".
|
| 313 |
+
# accelerator.prepare is still a good way to manage models.
|
| 314 |
+
qwen_model, rex_model = accelerator.prepare(qwen_model, rex_model)
|
| 315 |
+
|
| 316 |
+
print(f"[Process {accelerator.process_index}]: models loaded.")
|
| 317 |
+
except Exception as e:
|
| 318 |
+
print(f"[Process {accelerator.process_index}]: failed to load models: {e}")
|
| 319 |
+
import traceback
|
| 320 |
+
traceback.print_exc()
|
| 321 |
+
return
|
| 322 |
+
|
| 323 |
+
# 1. Define a batch size (Batch Size)
|
| 324 |
+
REX_BATCH_SIZE = 16 # <-- Adjust this value according to your VRAM
|
| 325 |
+
|
| 326 |
+
print(f"[Process {accelerator.process_index}]:"
|
| 327 |
+
f" starting to process {len(my_jobs)} jobs with Rex batch size {REX_BATCH_SIZE}.")
|
| 328 |
+
|
| 329 |
+
processed_metadata_list = []
|
| 330 |
+
|
| 331 |
+
# 2. Modify main loop: iterate with step REX_BATCH_SIZE
|
| 332 |
+
for i in tqdm(range(0, len(my_jobs), REX_BATCH_SIZE),
|
| 333 |
+
desc=f"Worker {accelerator.process_index} batch progress",
|
| 334 |
+
disable=not accelerator.is_main_process):
|
| 335 |
+
|
| 336 |
+
# 3. Prepare jobs and images for this batch
|
| 337 |
+
batch_jobs = my_jobs[i: i + REX_BATCH_SIZE]
|
| 338 |
+
batch_images = []
|
| 339 |
+
batch_image_paths = [] # for debugging
|
| 340 |
+
|
| 341 |
+
valid_jobs_in_batch = []
|
| 342 |
+
|
| 343 |
+
for job in batch_jobs:
|
| 344 |
+
try:
|
| 345 |
+
img_path = job['destination_image_path']
|
| 346 |
+
batch_image_paths.append(img_path)
|
| 347 |
+
batch_images.append(Image.open(img_path).convert("RGB"))
|
| 348 |
+
valid_jobs_in_batch.append(job) # only jobs with successfully loaded images are valid
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f"[Process {accelerator.process_index}]:"
|
| 351 |
+
f" failed to load image {img_path}: {e}, skipping this image in this batch.")
|
| 352 |
+
# We do not add the image or job, keeping batch_images and valid_jobs_in_batch in sync
|
| 353 |
+
|
| 354 |
+
if not batch_images: # if all images in this batch failed to load
|
| 355 |
+
continue
|
| 356 |
+
|
| 357 |
+
try:
|
| 358 |
+
# 4. ★ Key: run RexOmni in batch
|
| 359 |
+
# (we only pass successfully loaded images)
|
| 360 |
+
all_rex_results = rex_model.inference(
|
| 361 |
+
images=batch_images, # pass the image list
|
| 362 |
+
task="detection",
|
| 363 |
+
categories=["anything"]
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
# 5. Iterate over results in this batch
|
| 367 |
+
# all_rex_results length should equal batch_images (and valid_jobs_in_batch)
|
| 368 |
+
if len(all_rex_results) != len(valid_jobs_in_batch):
|
| 369 |
+
print(f"[Process {accelerator.process_index}]: Warning: RexOmni "
|
| 370 |
+
f"returned {len(all_rex_results)} results, but "
|
| 371 |
+
f"{len(valid_jobs_in_batch)} inputs were provided. Skipping this batch.")
|
| 372 |
+
continue
|
| 373 |
+
|
| 374 |
+
for job, image, rex_result in zip(valid_jobs_in_batch, batch_images, all_rex_results):
|
| 375 |
+
|
| 376 |
+
predictions = rex_result["extracted_predictions"]
|
| 377 |
+
detected_boxes = predictions.get("anything", [])
|
| 378 |
+
|
| 379 |
+
visual_facts = []
|
| 380 |
+
crops_to_process = []
|
| 381 |
+
box_coords_list = []
|
| 382 |
+
|
| 383 |
+
# 6. Collect all crops to be processed (from this image)
|
| 384 |
+
for annotation in detected_boxes:
|
| 385 |
+
if annotation.get("type") == "box" and len(annotation.get("coords", [])) == 4:
|
| 386 |
+
coords = annotation["coords"]
|
| 387 |
+
crop_image = _crop_and_expand_box(image, coords)
|
| 388 |
+
crops_to_process.append(crop_image)
|
| 389 |
+
box_coords_list.append(coords)
|
| 390 |
+
|
| 391 |
+
# 7. Batch VQA (logic remains the same, still batch *per image* crops)
|
| 392 |
+
if crops_to_process:
|
| 393 |
+
json_str_list = _query_qwen_vl_BATCH(
|
| 394 |
+
crops_to_process, qwen_model, qwen_processor, accelerator
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
# 8. Iterate over batched results and parse (logic unchanged)
|
| 398 |
+
for json_str, coords in zip(json_str_list, box_coords_list):
|
| 399 |
+
json_obj_list = _parse_qwen_json(json_str)
|
| 400 |
+
if json_obj_list:
|
| 401 |
+
try:
|
| 402 |
+
obj_dict = json_obj_list[0]
|
| 403 |
+
obj_dict["bounding_box"] = [round(c, 2) for c in coords]
|
| 404 |
+
visual_facts.append(obj_dict)
|
| 405 |
+
except (IndexError, TypeError, KeyError) as e:
|
| 406 |
+
print(f"[Process {accelerator.process_index}]: "
|
| 407 |
+
f"Error while parsing batched result: {e} | JSON: {json_str}")
|
| 408 |
+
|
| 409 |
+
# 9. Aggregate results for this job (logic unchanged)
|
| 410 |
+
processed_metadata_list.append({
|
| 411 |
+
"question": job['prompt'],
|
| 412 |
+
"answer": job['answer'],
|
| 413 |
+
"question_wo_prompt": job['prompt'],
|
| 414 |
+
"hint": job['hint'],
|
| 415 |
+
"image": job['destination_image_path'],
|
| 416 |
+
"visual_fact": visual_facts
|
| 417 |
+
})
|
| 418 |
+
# --- End of inner loop logic ---
|
| 419 |
+
|
| 420 |
+
except Exception as e:
|
| 421 |
+
print(f"[Process {accelerator.process_index}]: "
|
| 422 |
+
f"Error while processing batch {i // REX_BATCH_SIZE} (images {batch_image_paths}): {e}")
|
| 423 |
+
import traceback
|
| 424 |
+
traceback.print_exc()
|
| 425 |
+
|
| 426 |
+
# --- End of loop ---
|
| 427 |
+
|
| 428 |
+
print(f"[Process {accelerator.process_index}]:"
|
| 429 |
+
f" process finished, handled {len(processed_metadata_list)} items.")
|
| 430 |
+
|
| 431 |
+
# (7. Gather all results - unchanged)
|
| 432 |
+
print(f"[Process {accelerator.process_index}]: gathering results...")
|
| 433 |
+
all_results_list_of_lists = gather_object(processed_metadata_list)
|
| 434 |
+
|
| 435 |
+
# (8. Save (only on main process) - ★★★ using fixed GATHER logic ★★★)
|
| 436 |
+
if accelerator.is_main_process:
|
| 437 |
+
print("Main process [Saving]: aggregating and saving all results...")
|
| 438 |
+
|
| 439 |
+
# --- Key fix ---
|
| 440 |
+
# gather_object already returns a flattened list of dictionaries (List[dict]).
|
| 441 |
+
final_metadata_list = all_results_list_of_lists
|
| 442 |
+
# --- End of fix ---
|
| 443 |
+
|
| 444 |
+
json_filename = os.path.join(JSON_OUTPUT_DIR, "clevr_cogent_trainA_r1_processed.json")
|
| 445 |
+
|
| 446 |
+
# Verify the count
|
| 447 |
+
print(f"Total number of aggregated items: {len(final_metadata_list)}")
|
| 448 |
+
if len(final_metadata_list) > 0:
|
| 449 |
+
print(f"Type of first item: {type(final_metadata_list[0])}")
|
| 450 |
+
|
| 451 |
+
print(f"\nSaving {len(final_metadata_list)} metadata entries to {json_filename}...")
|
| 452 |
+
with open(json_filename, 'w', encoding='utf-8') as f:
|
| 453 |
+
json.dump(final_metadata_list, f, indent=4, ensure_ascii=False)
|
| 454 |
+
|
| 455 |
+
print(f"\n--- Processing completed! ---")
|
| 456 |
+
print(f"All image files have been saved in: '{IMAGE_OUTPUT_DIR}'")
|
| 457 |
+
print(f"Final JSON file has been saved in: '{JSON_OUTPUT_DIR}'")
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
if __name__ == "__main__":
|
| 461 |
+
main()
|
data_utils/clevr/run_single_test.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
from transformers import activations
|
| 4 |
+
activations.PytorchGELUTanh = activations.GELUTanh
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from datasets import load_dataset
|
| 9 |
+
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
|
| 10 |
+
|
| 11 |
+
# Import model wrapper
|
| 12 |
+
try:
|
| 13 |
+
from rex_omni import RexOmniWrapper
|
| 14 |
+
except ImportError:
|
| 15 |
+
# Import DummyRex matching the one in clevr_processor.py
|
| 16 |
+
print("Warning: 'from rex_omni import RexOmniWrapper' failed.")
|
| 17 |
+
print("Using a dummy RexOmniWrapper (DummyRex) for testing only.")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class DummyRex:
|
| 21 |
+
def __init__(self, *args, **kwargs):
|
| 22 |
+
print("INFO: DUMMY: Using DummyRex detector.")
|
| 23 |
+
|
| 24 |
+
def inference(self, images, task, categories, **kwargs):
|
| 25 |
+
print("INFO: DUMMY: DummyRex returning a fake center box.")
|
| 26 |
+
if isinstance(images, Image.Image):
|
| 27 |
+
w, h = images.size
|
| 28 |
+
else:
|
| 29 |
+
w, h = 800, 600
|
| 30 |
+
x0, y0 = w * 0.25, h * 0.25
|
| 31 |
+
x1, y1 = w * 0.75, h * 0.75
|
| 32 |
+
return [{"extracted_predictions": {"anything": [{"type": "box", "coords": [x0, y0, x1, y1]}]}}]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
RexOmniWrapper = DummyRex
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
from qwen_vl_utils import process_vision_info
|
| 39 |
+
except ImportError:
|
| 40 |
+
print("Warning: Failed to import 'qwen_vl_utils.process_vision_info'.")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def process_vision_info(messages):
|
| 44 |
+
images = []
|
| 45 |
+
for msg in messages:
|
| 46 |
+
if msg['role'] == 'user':
|
| 47 |
+
for content in msg['content']:
|
| 48 |
+
if content['type'] == 'image':
|
| 49 |
+
images.append(content['image'])
|
| 50 |
+
return images, None
|
| 51 |
+
|
| 52 |
+
from clevr_processor import ClevrFactExtractor, _strip_tags
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def run_test(configs, paths, gpu_id=0, sample_index=0):
|
| 56 |
+
"""
|
| 57 |
+
Run the test pipeline on a single sample.
|
| 58 |
+
"""
|
| 59 |
+
print("--- Starting single-run test (CoGenT) ---")
|
| 60 |
+
|
| 61 |
+
# --- 1. Set environment and load models ---
|
| 62 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
|
| 63 |
+
print(f"Set CUDA_VISIBLE_DEVICES={gpu_id}")
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
print(f"Loading RexOmni... ({configs['rex_path']})")
|
| 67 |
+
rex_model = RexOmniWrapper(
|
| 68 |
+
model_path=configs['rex_path'],
|
| 69 |
+
backend="transformers",
|
| 70 |
+
max_tokens=2048,
|
| 71 |
+
temperature=0.0,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
print(f"Loading Qwen-VL... ({configs['qwen_path']})")
|
| 75 |
+
qwen_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
| 76 |
+
configs['qwen_path'],
|
| 77 |
+
torch_dtype="float16",
|
| 78 |
+
device_map="cuda:0",
|
| 79 |
+
attn_implementation="flash_attention_2"
|
| 80 |
+
)
|
| 81 |
+
qwen_processor = AutoProcessor.from_pretrained(configs['qwen_path'])
|
| 82 |
+
|
| 83 |
+
print("Models loaded.")
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"Failed to load models: {e}")
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
print("Loading dataset metadata...")
|
| 89 |
+
try:
|
| 90 |
+
dataset = load_dataset("MMInstruction/Clevr_CoGenT_TrainA_R1", split='train', streaming=True)
|
| 91 |
+
example_iter = iter(dataset)
|
| 92 |
+
for _ in range(sample_index + 1):
|
| 93 |
+
example = next(example_iter)
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"Failed to load or filter dataset: {e}")
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
print(f"Processing sample {sample_index}...")
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# 1. Preprocessing
|
| 103 |
+
prompt = example['problem']
|
| 104 |
+
hint = _strip_tags(example['thinking'], 'think')
|
| 105 |
+
answer = _strip_tags(example['solution'], 'answer')
|
| 106 |
+
image = example['image'].convert("RGB") # Get PIL image and convert to RGB
|
| 107 |
+
|
| 108 |
+
# Save image for testing
|
| 109 |
+
destination_image_path = os.path.join(paths['output_dir'], "images", f"test_sample_{sample_index}.jpg")
|
| 110 |
+
os.makedirs(os.path.dirname(destination_image_path), exist_ok=True)
|
| 111 |
+
image.save(destination_image_path, "JPEG")
|
| 112 |
+
print(f"Loaded and saved test image: {destination_image_path}")
|
| 113 |
+
|
| 114 |
+
# --- Stage 1: RexOmni detection ---
|
| 115 |
+
print("Running RexOmni detection...")
|
| 116 |
+
rex_results = rex_model.inference(images=image, task="detection", categories=["anything"])
|
| 117 |
+
predictions = rex_results[0]["extracted_predictions"]
|
| 118 |
+
detected_boxes = predictions.get("anything", [])
|
| 119 |
+
print(f"RexOmni detected {len(detected_boxes)} 'anything' boxes.")
|
| 120 |
+
|
| 121 |
+
visual_facts = []
|
| 122 |
+
|
| 123 |
+
# --- Stage 2: Qwen-VL VQA ---
|
| 124 |
+
for i, annotation in enumerate(detected_boxes):
|
| 125 |
+
if annotation.get("type") == "box" and len(annotation.get("coords", [])) == 4:
|
| 126 |
+
|
| 127 |
+
coords = annotation["coords"]
|
| 128 |
+
print(f" Processing box {i}: {coords}")
|
| 129 |
+
|
| 130 |
+
crop_image = ClevrFactExtractor._crop_and_expand_box(image, coords)
|
| 131 |
+
|
| 132 |
+
# Save cropped image for debugging
|
| 133 |
+
crop_filename = f"./test_crop_{sample_index}_{i}.jpg"
|
| 134 |
+
crop_image.save(crop_filename)
|
| 135 |
+
print(f" -> Saved cropped image for inspection: {crop_filename}")
|
| 136 |
+
|
| 137 |
+
json_str = ClevrFactExtractor._query_qwen_vl(
|
| 138 |
+
crop_image, qwen_model, qwen_processor
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
json_obj_list = ClevrFactExtractor._parse_qwen_json(json_str)
|
| 142 |
+
|
| 143 |
+
if json_obj_list:
|
| 144 |
+
obj_dict = json_obj_list[0]
|
| 145 |
+
obj_dict["bounding_box"] = [round(c, 2) for c in coords]
|
| 146 |
+
visual_facts.append(obj_dict)
|
| 147 |
+
print(f" -> Qwen-VL result: {obj_dict}")
|
| 148 |
+
else:
|
| 149 |
+
print(f" -> Qwen-VL did not return valid JSON.")
|
| 150 |
+
|
| 151 |
+
# --- 4. Print final result ---
|
| 152 |
+
final_result = {
|
| 153 |
+
"prompt": prompt,
|
| 154 |
+
"answer": answer,
|
| 155 |
+
"hint": hint,
|
| 156 |
+
"image": destination_image_path,
|
| 157 |
+
"visual_fact": visual_facts
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
print("\n" + "=" * 30)
|
| 161 |
+
print("--- Single test result ---")
|
| 162 |
+
print(json.dumps(final_result, indent=4, ensure_ascii=False))
|
| 163 |
+
print("=" * 30 + "\n")
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"Error while processing sample {sample_index}: {e}")
|
| 167 |
+
import traceback
|
| 168 |
+
traceback.print_exc()
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == "__main__":
|
| 172 |
+
# --- 1. Model configs ---
|
| 173 |
+
MODEL_CONFIGS = {
|
| 174 |
+
"rex_path": "IDEA-Research/Rex-Omni",
|
| 175 |
+
"qwen_path": "Qwen/Qwen2.5-VL-32B-Instruct-AWQ"
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
# --- 2. Paths config ---
|
| 179 |
+
PATHS = {
|
| 180 |
+
# !! Change this to the directory where you want to save images and JSON !!
|
| 181 |
+
"output_dir": "./clevr_cogent_output"
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
# --- 3. Test parameters ---
|
| 185 |
+
GPU_ID_TO_USE = 0
|
| 186 |
+
SAMPLE_INDEX_TO_TEST = 0 # Test the first CLEVR sample
|
| 187 |
+
|
| 188 |
+
# --- 4. Run test ---
|
| 189 |
+
run_test(
|
| 190 |
+
configs=MODEL_CONFIGS,
|
| 191 |
+
paths=PATHS,
|
| 192 |
+
gpu_id=GPU_ID_TO_USE,
|
| 193 |
+
sample_index=SAMPLE_INDEX_TO_TEST
|
| 194 |
+
)
|
data_utils/paths.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Project-relative data paths for DyME."""
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| 5 |
+
|
| 6 |
+
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
|
| 7 |
+
DATA_IMAGES_DIR = os.path.join(DATA_DIR, "images")
|
| 8 |
+
|
| 9 |
+
CHARTQA_DIR = os.path.join(DATA_IMAGES_DIR, "chartqa")
|
| 10 |
+
CHARTQA_IMAGES_DIR = os.path.join(CHARTQA_DIR, "images")
|
| 11 |
+
CHARTQA_JSON_DIR = os.path.join(CHARTQA_DIR, "json")
|
| 12 |
+
|
| 13 |
+
AOKVQA_DIR = os.path.join(DATA_IMAGES_DIR, "aokvqa")
|
| 14 |
+
AOKVQA_IMAGES_DIR = os.path.join(AOKVQA_DIR, "images")
|
| 15 |
+
AOKVQA_JSON_DIR = os.path.join(AOKVQA_DIR, "json")
|
| 16 |
+
|
| 17 |
+
OUTPUTS_DIR = os.path.join(PROJECT_ROOT, "outputs")
|
| 18 |
+
|
| 19 |
+
# Legacy absolute prefixes in JSON files -> canonical project directories
|
| 20 |
+
_LEGACY_PREFIX_MAP = [
|
| 21 |
+
("/chartqa_output/", CHARTQA_DIR + os.sep),
|
| 22 |
+
("/path/to/chartqa_output/", CHARTQA_DIR + os.sep),
|
| 23 |
+
("/path/to/data/chartqa_output/", CHARTQA_DIR + os.sep),
|
| 24 |
+
("/path/to/data/aokvqa/", AOKVQA_DIR + os.sep),
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def project_path(*parts: str) -> str:
|
| 29 |
+
return os.path.join(PROJECT_ROOT, *parts)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def resolve_image_path(path: str) -> str:
|
| 33 |
+
"""Resolve a stored image path to an existing file under the project tree."""
|
| 34 |
+
if not path:
|
| 35 |
+
return path
|
| 36 |
+
if os.path.exists(path):
|
| 37 |
+
return path
|
| 38 |
+
|
| 39 |
+
candidates = []
|
| 40 |
+
for old, new in _LEGACY_PREFIX_MAP:
|
| 41 |
+
if old in path:
|
| 42 |
+
candidates.append(path.replace(old, new))
|
| 43 |
+
|
| 44 |
+
if not os.path.isabs(path):
|
| 45 |
+
candidates.append(os.path.join(PROJECT_ROOT, path))
|
| 46 |
+
|
| 47 |
+
basename = os.path.basename(path)
|
| 48 |
+
candidates.extend([
|
| 49 |
+
os.path.join(CHARTQA_IMAGES_DIR, basename),
|
| 50 |
+
os.path.join(AOKVQA_IMAGES_DIR, basename),
|
| 51 |
+
os.path.join(PROJECT_ROOT, "chartqa_output", "images", basename),
|
| 52 |
+
])
|
| 53 |
+
|
| 54 |
+
for candidate in candidates:
|
| 55 |
+
if os.path.exists(candidate):
|
| 56 |
+
return os.path.abspath(candidate)
|
| 57 |
+
|
| 58 |
+
return path
|
default_config_zero2.yaml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
compute_environment: LOCAL_MACHINE
|
| 2 |
+
debug: false
|
| 3 |
+
deepspeed_config:
|
| 4 |
+
deepspeed_config_file: configs/deepspeed/zero2_bf16.json
|
| 5 |
+
zero3_init_flag: false
|
| 6 |
+
distributed_type: DEEPSPEED
|
| 7 |
+
downcast_bf16: 'no'
|
| 8 |
+
enable_cpu_affinity: false
|
| 9 |
+
machine_rank: 0
|
| 10 |
+
main_training_function: main
|
| 11 |
+
num_machines: 1
|
| 12 |
+
# ZeRO-2 student sharding (HF Accelerate + DeepSpeed JSON). Pair with DYME_TEACHER_DEVICE_MAP=auto.
|
| 13 |
+
# Do NOT set mixed_precision / zero_stage here when using deepspeed_config_file (Accelerate requirement).
|
| 14 |
+
num_processes: 2
|
| 15 |
+
rdzv_backend: static
|
| 16 |
+
same_network: true
|
| 17 |
+
tpu_env: []
|
| 18 |
+
tpu_use_cluster: false
|
| 19 |
+
tpu_use_sudo: false
|
| 20 |
+
use_cpu: false
|
main_7B.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# train_grpo.py
|
| 2 |
+
"""
|
| 3 |
+
Main script for training a Llava-based model using the custom MyGRPOTrainer.
|
| 4 |
+
|
| 5 |
+
This script handles:
|
| 6 |
+
1. Configuration loading.
|
| 7 |
+
2. Initialization of Weights & Biases (wandb) and Hugging Face Accelerate.
|
| 8 |
+
3. Loading the model (with PEFT/LoRA) and processor.
|
| 9 |
+
4. Preparing the training and evaluation datasets.
|
| 10 |
+
5. Setting up and running the GPRO trainer.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
from functools import partial
|
| 15 |
+
from typing import Dict, Any
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import wandb
|
| 19 |
+
from accelerate import Accelerator
|
| 20 |
+
from datasets import Dataset, load_dataset
|
| 21 |
+
from transformers import AutoProcessor, Qwen2_5_VLForConditionalGeneration
|
| 22 |
+
from trl import GRPOConfig
|
| 23 |
+
from peft import LoraConfig, get_peft_model, TaskType # NEW: Import PEFT modules
|
| 24 |
+
|
| 25 |
+
from config.config_7B import CONFIG
|
| 26 |
+
from data_utils.commom_util import collate_fn, define_task_data_func
|
| 27 |
+
from trainer.DyMETrainer_7B import DyMETrainer
|
| 28 |
+
from reward_utils.checker import RewardCalculator, RewardCalculatorLocal
|
| 29 |
+
from reward_utils.refiner import ContextRefiner, ContextRefinerLocal
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def setup_accelerator_and_wandb(bf16) -> Accelerator:
|
| 33 |
+
"""
|
| 34 |
+
Initializes Weights & Biases and the Hugging Face Accelerator.
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
Accelerator: The configured accelerator instance.
|
| 38 |
+
"""
|
| 39 |
+
wandb_key = os.environ.get("WANDB_API_KEY")
|
| 40 |
+
if wandb_key:
|
| 41 |
+
wandb.login(key=wandb_key)
|
| 42 |
+
if bf16:
|
| 43 |
+
accelerator = Accelerator(mixed_precision="bf16", log_with="wandb")
|
| 44 |
+
else:
|
| 45 |
+
accelerator = Accelerator(log_with="wandb")
|
| 46 |
+
return accelerator
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# NEW: Updated function to accept peft_config
|
| 50 |
+
def load_model_and_processor(model_config: Dict[str, Any], peft_config: Dict[str, Any]):
|
| 51 |
+
"""
|
| 52 |
+
Loads the pre-trained vision-language model, applies PEFT/LoRA, and loads its processor.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
model_config (Dict[str, Any]): Configuration dictionary for the model.
|
| 56 |
+
peft_config (Dict[str, Any]): Configuration dictionary for PEFT/LoRA.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Tuple[PeftModel, PreTrainedProcessor]: The loaded PEFT-enabled model and processor.
|
| 60 |
+
"""
|
| 61 |
+
model_id = model_config['pretrained_model_path']
|
| 62 |
+
|
| 63 |
+
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
| 64 |
+
model_id,
|
| 65 |
+
torch_dtype=getattr(torch, model_config['torch_dtype']),
|
| 66 |
+
attn_implementation='flash_attention_2' if model_config['use_flash_attention_2'] else 'sdpa',
|
| 67 |
+
low_cpu_mem_usage=True,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Freeze the vision tower to save memory and computation
|
| 71 |
+
model.model.visual.requires_grad_(False)
|
| 72 |
+
|
| 73 |
+
# NEW: Create and apply LoRA configuration
|
| 74 |
+
print("Applying LoRA configuration...")
|
| 75 |
+
lora_config = peft_config
|
| 76 |
+
|
| 77 |
+
model = get_peft_model(model, lora_config)
|
| 78 |
+
|
| 79 |
+
# NEW: Print trainable parameters to verify LoRA is active
|
| 80 |
+
# This should show a very small percentage of trainable parameters.
|
| 81 |
+
model.print_trainable_parameters()
|
| 82 |
+
|
| 83 |
+
processor = AutoProcessor.from_pretrained(model_id)
|
| 84 |
+
processor.tokenizer.padding_side = "left"
|
| 85 |
+
# image_token_id = processor.tokenizer.additional_special_tokens_ids[
|
| 86 |
+
# processor.tokenizer.additional_special_tokens.index("<image>")]
|
| 87 |
+
|
| 88 |
+
return model, processor
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def prepare_datasets(task: str, dataset_config: Dict[str, Any]) -> (Dataset, Dataset):
|
| 92 |
+
"""
|
| 93 |
+
Prepares the training and evaluation datasets based on the specified task.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
task (str): The name of the task (e.g., 'chartqa').
|
| 97 |
+
dataset_config (Dict[str, Any]): Configuration for datasets.
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
Tuple[Dataset, Dataset]: The training and evaluation datasets.
|
| 101 |
+
"""
|
| 102 |
+
data_func = define_task_data_func(task)
|
| 103 |
+
|
| 104 |
+
# Create training dataset
|
| 105 |
+
train_data_list = data_func(json_path=dataset_config['train_dataset'])
|
| 106 |
+
train_dataset = Dataset.from_list(train_data_list)
|
| 107 |
+
|
| 108 |
+
# Create evaluation dataset
|
| 109 |
+
if 'chart' in task:
|
| 110 |
+
eval_dataset = load_dataset(dataset_config['eval_dataset'])['test']
|
| 111 |
+
# Note: You can uncomment the line below for quick testing/debugging.
|
| 112 |
+
# eval_dataset = eval_dataset.select(range(1000, 1100))
|
| 113 |
+
else:
|
| 114 |
+
# Extend this section for other tasks if needed in the future.
|
| 115 |
+
raise NotImplementedError(f"Task '{task}' is not supported for evaluation in this script.")
|
| 116 |
+
|
| 117 |
+
return train_dataset, eval_dataset
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def main():
|
| 121 |
+
"""
|
| 122 |
+
Main function to orchestrate the model training pipeline.
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
# 1. Load Configurations
|
| 126 |
+
model_config = CONFIG['model']
|
| 127 |
+
training_config = CONFIG['training']
|
| 128 |
+
rl_config = CONFIG['rl']
|
| 129 |
+
client_config = CONFIG['client']
|
| 130 |
+
dataset_config = CONFIG['dataset']
|
| 131 |
+
peft_config = LoraConfig(
|
| 132 |
+
r=64,
|
| 133 |
+
lora_alpha=128,
|
| 134 |
+
lora_dropout=0.05,
|
| 135 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
| 136 |
+
use_rslora=True,
|
| 137 |
+
bias="none",
|
| 138 |
+
task_type="CAUSAL_LM",
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
task = training_config['task']
|
| 142 |
+
|
| 143 |
+
# 2. Setup Environment
|
| 144 |
+
accelerator = setup_accelerator_and_wandb(bf16=training_config['dyme_args']['bf16'])
|
| 145 |
+
device_id = accelerator.process_index
|
| 146 |
+
|
| 147 |
+
# 3. Initialize Model and Processor
|
| 148 |
+
# NEW: Pass peft_config to the loading function
|
| 149 |
+
model, processor = load_model_and_processor(model_config, peft_config)
|
| 150 |
+
|
| 151 |
+
# 4. Prepare Datasets
|
| 152 |
+
train_dataset, eval_dataset = prepare_datasets(task, dataset_config)
|
| 153 |
+
|
| 154 |
+
# 5. Initialize Reward Calculator
|
| 155 |
+
|
| 156 |
+
checker = RewardCalculatorLocal(rl_config, client_config.copy(), gpu_id=device_id)
|
| 157 |
+
refiner = ContextRefinerLocal(rl_config, client_config.copy(), gpu_id=device_id)
|
| 158 |
+
# 6. Define Training Arguments
|
| 159 |
+
training_args = GRPOConfig(**training_config['dyme_args'])
|
| 160 |
+
|
| 161 |
+
collate_fn_with_processor = partial(collate_fn, processor=processor)
|
| 162 |
+
# 7. Initialize the Trainer
|
| 163 |
+
dyme_trainer = DyMETrainer(
|
| 164 |
+
model=model, # NEW: This is now a PeftModel
|
| 165 |
+
checker=checker,
|
| 166 |
+
refiner=refiner,
|
| 167 |
+
args=training_args,
|
| 168 |
+
train_dataset=train_dataset,
|
| 169 |
+
eval_dataset=eval_dataset,
|
| 170 |
+
processing_class=processor,
|
| 171 |
+
processing_func=collate_fn_with_processor,
|
| 172 |
+
task_name=task,
|
| 173 |
+
end_flag=rl_config['end_flag'],
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# 8. Start Training
|
| 177 |
+
dyme_trainer.train()
|
| 178 |
+
|
| 179 |
+
output_dir = training_args.output_dir
|
| 180 |
+
output_dir = os.path.join(output_dir, "final_checkpoint")
|
| 181 |
+
|
| 182 |
+
# NEW: save_model will now save only the LoRA adapter weights
|
| 183 |
+
dyme_trainer.save_model(output_dir)
|
| 184 |
+
|
| 185 |
+
if accelerator.is_main_process:
|
| 186 |
+
processor.save_pretrained(output_dir)
|
| 187 |
+
# NEW: The saved model is just the adapter, not the full model.
|
| 188 |
+
print(f"LoRA adapter and processor saved to {output_dir}")
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
if __name__ == "__main__":
|
| 192 |
+
main()
|
opsd_utils/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from opsd_utils.constants import MODE_GRPO, MODE_OPSD, MODE_SFT
|
| 2 |
+
from opsd_utils.mode_router import route_prompt_modes, expand_modes_to_completions
|
| 3 |
+
from opsd_utils.opsd_loss import compute_vlm_opsd_loss
|
| 4 |
+
from opsd_utils.privileged import build_privileged_context
|
| 5 |
+
from opsd_utils.recoverability import estimate_recoverable_flags
|
| 6 |
+
from opsd_utils.prompt_builder import build_teacher_prompt_batch
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"MODE_GRPO",
|
| 10 |
+
"MODE_OPSD",
|
| 11 |
+
"MODE_SFT",
|
| 12 |
+
"route_prompt_modes",
|
| 13 |
+
"expand_modes_to_completions",
|
| 14 |
+
"compute_vlm_opsd_loss",
|
| 15 |
+
"build_privileged_context",
|
| 16 |
+
"estimate_recoverable_flags",
|
| 17 |
+
"build_teacher_prompt_batch",
|
| 18 |
+
]
|
opsd_utils/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (693 Bytes). View file
|
|
|
opsd_utils/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (731 Bytes). View file
|
|
|
opsd_utils/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (577 Bytes). View file
|
|
|
opsd_utils/__pycache__/debug_log.cpython-310.pyc
ADDED
|
Binary file (14.4 kB). View file
|
|
|
opsd_utils/__pycache__/health_monitor.cpython-310.pyc
ADDED
|
Binary file (12.9 kB). View file
|
|
|
opsd_utils/__pycache__/leakage.cpython-310.pyc
ADDED
|
Binary file (1.5 kB). View file
|
|
|
opsd_utils/__pycache__/mode_router.cpython-310.pyc
ADDED
|
Binary file (4.25 kB). View file
|
|
|
opsd_utils/__pycache__/mode_router.cpython-312.pyc
ADDED
|
Binary file (2.89 kB). View file
|
|
|
opsd_utils/__pycache__/opsd_loss.cpython-310.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
opsd_utils/__pycache__/prompt_builder.cpython-310.pyc
ADDED
|
Binary file (7.06 kB). View file
|
|
|
opsd_utils/__pycache__/recoverability.cpython-310.pyc
ADDED
|
Binary file (5.19 kB). View file
|
|
|
opsd_utils/__pycache__/recoverability.cpython-312.pyc
ADDED
|
Binary file (5.87 kB). View file
|
|
|
opsd_utils/__pycache__/vocab_align.cpython-310.pyc
ADDED
|
Binary file (5.47 kB). View file
|
|
|
opsd_utils/constants.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MODE_GRPO = 0
|
| 2 |
+
MODE_OPSD = 1
|
| 3 |
+
MODE_SFT = 2
|
| 4 |
+
|
| 5 |
+
DEFAULT_OPSD_CONFIG = {
|
| 6 |
+
"enabled": False,
|
| 7 |
+
"mode": "dyme",
|
| 8 |
+
"privileged_profile": "hybrid",
|
| 9 |
+
"privileged_providers": ["text"],
|
| 10 |
+
"privileged_image": {
|
| 11 |
+
"mode": "single",
|
| 12 |
+
"crop_strategy": "bbox_then_center",
|
| 13 |
+
"bbox_coord": "normalized",
|
| 14 |
+
"margin_ratio": 0.25,
|
| 15 |
+
},
|
| 16 |
+
"privileged_debug": {
|
| 17 |
+
"save_images": True,
|
| 18 |
+
"image_subdir": "logs/images",
|
| 19 |
+
"max_samples_per_detail": 2,
|
| 20 |
+
},
|
| 21 |
+
"gate": {
|
| 22 |
+
"correct_threshold": 0.5,
|
| 23 |
+
"teacher_recoverable": "privileged_available",
|
| 24 |
+
"recoverable_tau": 0.5,
|
| 25 |
+
"recoverable_without_privilege": False,
|
| 26 |
+
"use_edge_mask": False,
|
| 27 |
+
"per_completion_opsd": True,
|
| 28 |
+
"require_format_for_opsd": True,
|
| 29 |
+
"skip_degenerate_for_opsd": True,
|
| 30 |
+
},
|
| 31 |
+
"loss": {
|
| 32 |
+
"beta": 0.5,
|
| 33 |
+
"opsd_weight": 2.0,
|
| 34 |
+
"grpo_weight": 1.0,
|
| 35 |
+
"sft_weight": 1.0,
|
| 36 |
+
"acc_gate": True,
|
| 37 |
+
},
|
| 38 |
+
"text_include_gold": True,
|
| 39 |
+
"reward_weights": [1.0, 1.0, 1.0],
|
| 40 |
+
}
|
opsd_utils/debug_log.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verbose debug logging for the OPSD / TriMode training pipeline."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
import traceback
|
| 8 |
+
from contextlib import contextmanager
|
| 9 |
+
from typing import Any, Optional
|
| 10 |
+
|
| 11 |
+
_DEBUG_ENABLED = False
|
| 12 |
+
_DETAIL_EVERY = 10
|
| 13 |
+
_PROBE_ON_GENERATE = False
|
| 14 |
+
_PROBE_FIRST_TOKEN_LOGITS = True
|
| 15 |
+
_PROBE_PROMPT_TAIL_TOKENS = 16
|
| 16 |
+
_PROBE_LOG_MODEL_CONTEXT = True
|
| 17 |
+
_HEALTH_MONITOR_ENABLED = True
|
| 18 |
+
_HEALTH_LOG_ON_GENERATE = True
|
| 19 |
+
_HEALTH_LOG_EVERY_STEP = True
|
| 20 |
+
_HEALTH_LOG_DETAIL_BUNDLE = True
|
| 21 |
+
_HEALTH_LOG_ALERTS_IMMEDIATELY = True
|
| 22 |
+
_RANK = 0
|
| 23 |
+
_WORLD_SIZE = 1
|
| 24 |
+
_STEP_LABEL = "init"
|
| 25 |
+
_DETAIL_STEP: Optional[int] = None
|
| 26 |
+
_CALL_COUNTER = 0
|
| 27 |
+
|
| 28 |
+
MODE_NAMES = {0: "GRPO", 1: "OPSD", 2: "SFT"}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _env_debug_enabled() -> bool:
|
| 32 |
+
return os.environ.get("DYME_OPSD_DEBUG", "").strip().lower() in ("1", "true", "yes", "on")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _env_detail_every() -> int:
|
| 36 |
+
raw = os.environ.get("DYME_OPSD_DETAIL_EVERY", "").strip()
|
| 37 |
+
if not raw:
|
| 38 |
+
return 10
|
| 39 |
+
try:
|
| 40 |
+
return max(0, int(raw))
|
| 41 |
+
except ValueError:
|
| 42 |
+
return 10
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _env_probe_on_generate() -> Optional[bool]:
|
| 46 |
+
raw = os.environ.get("DYME_OPSD_PROBE_ON_GENERATE", "").strip().lower()
|
| 47 |
+
if not raw:
|
| 48 |
+
return None
|
| 49 |
+
return raw in ("1", "true", "yes", "on")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _env_probe_first_token_logits() -> Optional[bool]:
|
| 53 |
+
raw = os.environ.get("DYME_OPSD_PROBE_FIRST_TOKEN_LOGITS", "").strip().lower()
|
| 54 |
+
if not raw:
|
| 55 |
+
return None
|
| 56 |
+
return raw in ("1", "true", "yes", "on")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _env_probe_prompt_tail_tokens() -> Optional[int]:
|
| 60 |
+
raw = os.environ.get("DYME_OPSD_PROBE_PROMPT_TAIL_TOKENS", "").strip()
|
| 61 |
+
if not raw:
|
| 62 |
+
return None
|
| 63 |
+
try:
|
| 64 |
+
return max(1, int(raw))
|
| 65 |
+
except ValueError:
|
| 66 |
+
return 16
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _env_probe_log_model_context() -> Optional[bool]:
|
| 70 |
+
raw = os.environ.get("DYME_OPSD_PROBE_LOG_MODEL_CONTEXT", "").strip().lower()
|
| 71 |
+
if not raw:
|
| 72 |
+
return None
|
| 73 |
+
return raw in ("1", "true", "yes", "on")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _env_health_monitor_enabled() -> Optional[bool]:
|
| 77 |
+
raw = os.environ.get("DYME_OPSD_HEALTH_MONITOR", "").strip().lower()
|
| 78 |
+
if not raw:
|
| 79 |
+
return None
|
| 80 |
+
return raw in ("1", "true", "yes", "on")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def configure(
|
| 84 |
+
*,
|
| 85 |
+
enabled: Optional[bool] = None,
|
| 86 |
+
detail_every: Optional[int] = None,
|
| 87 |
+
probe_on_generate: Optional[bool] = None,
|
| 88 |
+
probe_first_token_logits: Optional[bool] = None,
|
| 89 |
+
probe_prompt_tail_tokens: Optional[int] = None,
|
| 90 |
+
probe_log_model_context: Optional[bool] = None,
|
| 91 |
+
health_monitor_enabled: Optional[bool] = None,
|
| 92 |
+
health_log_on_generate: Optional[bool] = None,
|
| 93 |
+
health_log_every_step: Optional[bool] = None,
|
| 94 |
+
health_log_detail_bundle: Optional[bool] = None,
|
| 95 |
+
health_log_alerts_immediately: Optional[bool] = None,
|
| 96 |
+
rank: Optional[int] = None,
|
| 97 |
+
world_size: Optional[int] = None,
|
| 98 |
+
) -> bool:
|
| 99 |
+
"""Configure global OPSD debug logging. Returns whether debug is enabled."""
|
| 100 |
+
global _DEBUG_ENABLED, _DETAIL_EVERY, _PROBE_ON_GENERATE
|
| 101 |
+
global _PROBE_FIRST_TOKEN_LOGITS, _PROBE_PROMPT_TAIL_TOKENS, _PROBE_LOG_MODEL_CONTEXT
|
| 102 |
+
global _HEALTH_MONITOR_ENABLED, _HEALTH_LOG_ON_GENERATE, _HEALTH_LOG_EVERY_STEP
|
| 103 |
+
global _HEALTH_LOG_DETAIL_BUNDLE, _HEALTH_LOG_ALERTS_IMMEDIATELY
|
| 104 |
+
global _RANK, _WORLD_SIZE
|
| 105 |
+
if enabled is None:
|
| 106 |
+
enabled = _env_debug_enabled()
|
| 107 |
+
_DEBUG_ENABLED = bool(enabled)
|
| 108 |
+
if detail_every is not None:
|
| 109 |
+
_DETAIL_EVERY = max(0, int(detail_every))
|
| 110 |
+
elif _env_detail_every() != 10 or os.environ.get("DYME_OPSD_DETAIL_EVERY"):
|
| 111 |
+
_DETAIL_EVERY = _env_detail_every()
|
| 112 |
+
env_probe = _env_probe_on_generate()
|
| 113 |
+
if probe_on_generate is not None:
|
| 114 |
+
_PROBE_ON_GENERATE = bool(probe_on_generate)
|
| 115 |
+
elif env_probe is not None:
|
| 116 |
+
_PROBE_ON_GENERATE = env_probe
|
| 117 |
+
env_first_logits = _env_probe_first_token_logits()
|
| 118 |
+
if probe_first_token_logits is not None:
|
| 119 |
+
_PROBE_FIRST_TOKEN_LOGITS = bool(probe_first_token_logits)
|
| 120 |
+
elif env_first_logits is not None:
|
| 121 |
+
_PROBE_FIRST_TOKEN_LOGITS = env_first_logits
|
| 122 |
+
env_tail = _env_probe_prompt_tail_tokens()
|
| 123 |
+
if probe_prompt_tail_tokens is not None:
|
| 124 |
+
_PROBE_PROMPT_TAIL_TOKENS = max(1, int(probe_prompt_tail_tokens))
|
| 125 |
+
elif env_tail is not None:
|
| 126 |
+
_PROBE_PROMPT_TAIL_TOKENS = env_tail
|
| 127 |
+
env_model_ctx = _env_probe_log_model_context()
|
| 128 |
+
if probe_log_model_context is not None:
|
| 129 |
+
_PROBE_LOG_MODEL_CONTEXT = bool(probe_log_model_context)
|
| 130 |
+
elif env_model_ctx is not None:
|
| 131 |
+
_PROBE_LOG_MODEL_CONTEXT = env_model_ctx
|
| 132 |
+
env_health = _env_health_monitor_enabled()
|
| 133 |
+
if health_monitor_enabled is not None:
|
| 134 |
+
_HEALTH_MONITOR_ENABLED = bool(health_monitor_enabled)
|
| 135 |
+
elif env_health is not None:
|
| 136 |
+
_HEALTH_MONITOR_ENABLED = env_health
|
| 137 |
+
if health_log_on_generate is not None:
|
| 138 |
+
_HEALTH_LOG_ON_GENERATE = bool(health_log_on_generate)
|
| 139 |
+
if health_log_every_step is not None:
|
| 140 |
+
_HEALTH_LOG_EVERY_STEP = bool(health_log_every_step)
|
| 141 |
+
if health_log_detail_bundle is not None:
|
| 142 |
+
_HEALTH_LOG_DETAIL_BUNDLE = bool(health_log_detail_bundle)
|
| 143 |
+
if health_log_alerts_immediately is not None:
|
| 144 |
+
_HEALTH_LOG_ALERTS_IMMEDIATELY = bool(health_log_alerts_immediately)
|
| 145 |
+
if rank is not None:
|
| 146 |
+
_RANK = rank
|
| 147 |
+
if world_size is not None:
|
| 148 |
+
_WORLD_SIZE = world_size
|
| 149 |
+
return _DEBUG_ENABLED
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def detail_every() -> int:
|
| 153 |
+
return _DETAIL_EVERY
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def probe_on_generate() -> bool:
|
| 157 |
+
return _PROBE_ON_GENERATE
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def probe_first_token_logits() -> bool:
|
| 161 |
+
return _PROBE_FIRST_TOKEN_LOGITS
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def probe_prompt_tail_tokens() -> int:
|
| 165 |
+
return _PROBE_PROMPT_TAIL_TOKENS
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def probe_log_model_context() -> bool:
|
| 169 |
+
return _PROBE_LOG_MODEL_CONTEXT
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def should_log_probe() -> bool:
|
| 173 |
+
"""True when lightweight per-generate probe should run (rank 0 only)."""
|
| 174 |
+
return _PROBE_ON_GENERATE and _RANK == 0
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def should_log_detail(global_step: Optional[int]) -> bool:
|
| 178 |
+
"""True when a full diagnostic bundle should be emitted (rank 0 only)."""
|
| 179 |
+
if _DETAIL_EVERY <= 0 or _RANK != 0:
|
| 180 |
+
return False
|
| 181 |
+
if global_step is None:
|
| 182 |
+
return False
|
| 183 |
+
return int(global_step) % _DETAIL_EVERY == 0
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def is_enabled() -> bool:
|
| 187 |
+
return _DEBUG_ENABLED
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def set_step_label(label: str) -> None:
|
| 191 |
+
global _STEP_LABEL
|
| 192 |
+
_STEP_LABEL = label
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def set_detail_step(global_step: Optional[int]) -> None:
|
| 196 |
+
global _DETAIL_STEP
|
| 197 |
+
_DETAIL_STEP = global_step
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def get_detail_step() -> Optional[int]:
|
| 201 |
+
return _DETAIL_STEP
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _next_call_id(stage: str) -> str:
|
| 205 |
+
global _CALL_COUNTER
|
| 206 |
+
_CALL_COUNTER += 1
|
| 207 |
+
return f"{_CALL_COUNTER}:{stage}"
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _fmt(value: Any, max_len: int = 240) -> str:
|
| 211 |
+
if value is None:
|
| 212 |
+
return "None"
|
| 213 |
+
try:
|
| 214 |
+
import torch
|
| 215 |
+
|
| 216 |
+
if isinstance(value, torch.Tensor):
|
| 217 |
+
return (
|
| 218 |
+
f"Tensor(shape={tuple(value.shape)}, dtype={value.dtype}, "
|
| 219 |
+
f"device={value.device}, numel={value.numel()})"
|
| 220 |
+
)
|
| 221 |
+
except ImportError:
|
| 222 |
+
pass
|
| 223 |
+
if isinstance(value, (list, tuple)):
|
| 224 |
+
if len(value) > 12:
|
| 225 |
+
head = ", ".join(_fmt(v, max_len=40) for v in value[:6])
|
| 226 |
+
return f"[{head}, ... +{len(value) - 6} more, total={len(value)}]"
|
| 227 |
+
return "[" + ", ".join(_fmt(v, max_len=40) for v in value) + "]"
|
| 228 |
+
if isinstance(value, dict):
|
| 229 |
+
text = json.dumps(value, ensure_ascii=False, default=str)
|
| 230 |
+
else:
|
| 231 |
+
text = repr(value)
|
| 232 |
+
if len(text) > max_len:
|
| 233 |
+
return text[: max_len - 3] + "..."
|
| 234 |
+
return text
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _prefix(stage: str, call_id: Optional[str] = None) -> str:
|
| 238 |
+
cid = call_id or _next_call_id(stage)
|
| 239 |
+
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 240 |
+
return f"[OPSD-DEBUG][{ts}][rank={_RANK}/{_WORLD_SIZE}][step={_STEP_LABEL}][{cid}]"
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def log(stage: str, msg: str, **fields: Any) -> None:
|
| 244 |
+
if not _DEBUG_ENABLED:
|
| 245 |
+
return
|
| 246 |
+
call_id = _next_call_id(stage)
|
| 247 |
+
extra = ""
|
| 248 |
+
if fields:
|
| 249 |
+
extra = " | " + " | ".join(f"{k}={_fmt(v)}" for k, v in fields.items())
|
| 250 |
+
print(f"{_prefix(stage, call_id)} {msg}{extra}", flush=True)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def _detail_prefix(global_step: int, section: str) -> str:
|
| 254 |
+
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 255 |
+
return (
|
| 256 |
+
f"[OPSD-DETAIL][{ts}][rank={_RANK}/{_WORLD_SIZE}]"
|
| 257 |
+
f"[step={global_step}][every={_DETAIL_EVERY}][{section}]"
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def log_detail_banner(global_step: int, title: str) -> None:
|
| 262 |
+
if not should_log_detail(global_step):
|
| 263 |
+
return
|
| 264 |
+
bar = "=" * 20
|
| 265 |
+
print(f"{_detail_prefix(global_step, 'BANNER')} {bar} {title} {bar}", flush=True)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def log_detail(section: str, msg: str, global_step: Optional[int] = None, **fields: Any) -> None:
|
| 269 |
+
"""Full-detail diagnostic line (periodic, rank 0). Independent of verbose OPSD-DEBUG."""
|
| 270 |
+
step = global_step if global_step is not None else _DETAIL_STEP
|
| 271 |
+
if step is None or isinstance(step, str):
|
| 272 |
+
return
|
| 273 |
+
if not should_log_detail(step):
|
| 274 |
+
return
|
| 275 |
+
extra = ""
|
| 276 |
+
if fields:
|
| 277 |
+
extra = " | " + " | ".join(f"{k}={_fmt(v, max_len=800)}" for k, v in fields.items())
|
| 278 |
+
print(f"{_detail_prefix(step, section)} {msg}{extra}", flush=True)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def _probe_prefix(section: str) -> str:
|
| 282 |
+
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 283 |
+
step = _DETAIL_STEP if _DETAIL_STEP is not None else "?"
|
| 284 |
+
return (
|
| 285 |
+
f"[OPSD-PROBE][{ts}][rank={_RANK}/{_WORLD_SIZE}]"
|
| 286 |
+
f"[global_step={step}][{_STEP_LABEL}][{section}]"
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def log_probe(section: str, msg: str, **fields: Any) -> None:
|
| 291 |
+
"""Lightweight per-generate diagnostic (rank 0). Independent of OPSD-DEBUG verbosity."""
|
| 292 |
+
if not should_log_probe():
|
| 293 |
+
return
|
| 294 |
+
extra = ""
|
| 295 |
+
if fields:
|
| 296 |
+
extra = " | " + " | ".join(f"{k}={_fmt(v, max_len=1200)}" for k, v in fields.items())
|
| 297 |
+
print(f"{_probe_prefix(section)} {msg}{extra}", flush=True)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _gendbg_prefix(section: str) -> str:
|
| 301 |
+
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 302 |
+
step = _DETAIL_STEP if _DETAIL_STEP is not None else "?"
|
| 303 |
+
return (
|
| 304 |
+
f"[OPSD-GENDBG][{ts}][rank={_RANK}/{_WORLD_SIZE}]"
|
| 305 |
+
f"[global_step={step}][{_STEP_LABEL}][{section}]"
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def should_log_gendbg() -> bool:
|
| 310 |
+
"""True when deep generate diagnostics should run (rank 0 only)."""
|
| 311 |
+
return _PROBE_ON_GENERATE and _RANK == 0
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def health_monitor_enabled() -> bool:
|
| 315 |
+
return _HEALTH_MONITOR_ENABLED and _RANK == 0
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def should_log_health_on_generate() -> bool:
|
| 319 |
+
return health_monitor_enabled() and _HEALTH_LOG_ON_GENERATE and should_log_probe()
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def should_log_health_every_step() -> bool:
|
| 323 |
+
return health_monitor_enabled() and _HEALTH_LOG_EVERY_STEP
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def should_log_health_detail_bundle() -> bool:
|
| 327 |
+
return health_monitor_enabled() and _HEALTH_LOG_DETAIL_BUNDLE
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def should_log_health_alerts_immediately() -> bool:
|
| 331 |
+
return health_monitor_enabled() and _HEALTH_LOG_ALERTS_IMMEDIATELY
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def _health_prefix(section: str, global_step: Optional[int] = None) -> str:
|
| 335 |
+
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
| 336 |
+
step = global_step if global_step is not None else (_DETAIL_STEP if _DETAIL_STEP is not None else "?")
|
| 337 |
+
return f"[OPSD-HEALTH][{ts}][rank={_RANK}/{_WORLD_SIZE}][global_step={step}][{section}]"
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def log_health(section: str, msg: str, global_step: Optional[int] = None, **fields: Any) -> None:
|
| 341 |
+
"""L1/L2/L4 health lines (rank 0)."""
|
| 342 |
+
if not health_monitor_enabled():
|
| 343 |
+
return
|
| 344 |
+
extra = ""
|
| 345 |
+
if fields:
|
| 346 |
+
extra = " | " + " | ".join(f"{k}={_fmt(v, max_len=1200)}" for k, v in fields.items())
|
| 347 |
+
print(f"{_health_prefix(section, global_step)} {msg}{extra}", flush=True)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def log_health_detail_banner(global_step: int, title: str) -> None:
|
| 351 |
+
if not should_log_health_detail_bundle() or not should_log_detail(global_step):
|
| 352 |
+
return
|
| 353 |
+
bar = "=" * 20
|
| 354 |
+
print(f"{_detail_prefix(global_step, 'health')} {bar} {title} {bar}", flush=True)
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def log_health_detail(section: str, msg: str, global_step: int, **fields: Any) -> None:
|
| 358 |
+
"""L3 periodic health bundle (rank 0, same cadence as OPSD-DETAIL)."""
|
| 359 |
+
if not should_log_health_detail_bundle() or not should_log_detail(global_step):
|
| 360 |
+
return
|
| 361 |
+
extra = ""
|
| 362 |
+
if fields:
|
| 363 |
+
extra = " | " + " | ".join(f"{k}={_fmt(v, max_len=1200)}" for k, v in fields.items())
|
| 364 |
+
print(f"{_detail_prefix(global_step, section)} {msg}{extra}", flush=True)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def log_gendbg(section: str, msg: str, **fields: Any) -> None:
|
| 368 |
+
"""Deep per-generate diagnostic (rank 0). Uses [OPSD-GENDBG] prefix."""
|
| 369 |
+
if not should_log_gendbg():
|
| 370 |
+
return
|
| 371 |
+
extra = ""
|
| 372 |
+
if fields:
|
| 373 |
+
extra = " | " + " | ".join(f"{k}={_fmt(v, max_len=1200)}" for k, v in fields.items())
|
| 374 |
+
print(f"{_gendbg_prefix(section)} {msg}{extra}", flush=True)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def log_config(stage: str, title: str, config: dict[str, Any]) -> None:
|
| 378 |
+
if not _DEBUG_ENABLED:
|
| 379 |
+
return
|
| 380 |
+
log(stage, title, config=_fmt(config, max_len=2000))
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def log_sync_point(stage: str, msg: str, **fields: Any) -> None:
|
| 384 |
+
"""Mark a point where all ranks must reach before a collective call."""
|
| 385 |
+
log(stage, f"[SYNC_POINT] {msg}", **fields)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def log_mode_summary(stage: str, prompt_modes: list[int], completion_modes: Optional[list[int]] = None) -> None:
|
| 389 |
+
if not _DEBUG_ENABLED:
|
| 390 |
+
return
|
| 391 |
+
prompt_names = [MODE_NAMES.get(m, str(m)) for m in prompt_modes]
|
| 392 |
+
fields: dict[str, Any] = {"prompt_modes": prompt_names}
|
| 393 |
+
if completion_modes is not None:
|
| 394 |
+
counts = {name: completion_modes.count(code) for code, name in MODE_NAMES.items()}
|
| 395 |
+
fields["completion_mode_counts"] = counts
|
| 396 |
+
log(stage, "mode routing summary", **fields)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def log_tensor(stage: str, name: str, tensor: Any) -> None:
|
| 400 |
+
if not _DEBUG_ENABLED:
|
| 401 |
+
return
|
| 402 |
+
log(stage, f"tensor `{name}`", value=_fmt(tensor))
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def log_exception(stage: str, msg: str, exc: BaseException) -> None:
|
| 406 |
+
if not _DEBUG_ENABLED:
|
| 407 |
+
return
|
| 408 |
+
tb = traceback.format_exc()
|
| 409 |
+
log(stage, msg, error=repr(exc), traceback=_fmt(tb, max_len=4000))
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
@contextmanager
|
| 413 |
+
def timed(stage: str, msg: str = "", **fields: Any):
|
| 414 |
+
if not _DEBUG_ENABLED:
|
| 415 |
+
yield
|
| 416 |
+
return
|
| 417 |
+
t0 = time.perf_counter()
|
| 418 |
+
log(stage, f"START {msg}", **fields)
|
| 419 |
+
try:
|
| 420 |
+
yield
|
| 421 |
+
except Exception as exc:
|
| 422 |
+
log_exception(stage, f"FAILED {msg}", exc)
|
| 423 |
+
raise
|
| 424 |
+
finally:
|
| 425 |
+
elapsed = time.perf_counter() - t0
|
| 426 |
+
log(stage, f"END {msg}", elapsed_s=f"{elapsed:.4f}")
|
opsd_utils/deepspeed_utils.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helpers for DeepSpeed + Accelerate launch detection."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _project_root() -> Path:
|
| 12 |
+
return Path(__file__).resolve().parents[1]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def resolve_accelerate_config_path(config_name: Optional[str] = None) -> Optional[Path]:
|
| 16 |
+
candidates: list[str] = []
|
| 17 |
+
if config_name:
|
| 18 |
+
candidates.append(str(config_name).strip())
|
| 19 |
+
for env_key in ("ACCELERATE_CONFIG", "ACCELERATE_CONFIG_FILE"):
|
| 20 |
+
val = os.environ.get(env_key, "").strip()
|
| 21 |
+
if val:
|
| 22 |
+
candidates.append(val)
|
| 23 |
+
for raw in candidates:
|
| 24 |
+
if not raw:
|
| 25 |
+
continue
|
| 26 |
+
path = Path(raw)
|
| 27 |
+
if not path.is_file():
|
| 28 |
+
path = _project_root() / raw
|
| 29 |
+
if path.is_file():
|
| 30 |
+
return path
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def uses_deepspeed_json_file(config_name: Optional[str] = None) -> bool:
|
| 35 |
+
"""True when Accelerate loads DeepSpeed settings from an external JSON file."""
|
| 36 |
+
path = resolve_accelerate_config_path(config_name)
|
| 37 |
+
if path is None:
|
| 38 |
+
return False
|
| 39 |
+
return "deepspeed_config_file" in path.read_text(encoding="utf-8")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _yaml_get_str(path: Path, key: str) -> Optional[str]:
|
| 43 |
+
pattern = re.compile(rf"^{re.escape(key)}\s*:\s*(.+?)\s*$", re.IGNORECASE)
|
| 44 |
+
for line in path.read_text(encoding="utf-8").splitlines():
|
| 45 |
+
m = pattern.match(line.strip())
|
| 46 |
+
if m:
|
| 47 |
+
return m.group(1).strip().strip("'\"")
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def is_deepspeed_accelerate_config(config_name: Optional[str] = None) -> bool:
|
| 52 |
+
path = resolve_accelerate_config_path(config_name)
|
| 53 |
+
if path is None:
|
| 54 |
+
return False
|
| 55 |
+
dist = (_yaml_get_str(path, "distributed_type") or "").upper()
|
| 56 |
+
return dist == "DEEPSPEED"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def deepspeed_zero_stage(config_name: Optional[str] = None) -> Optional[int]:
|
| 60 |
+
path = resolve_accelerate_config_path(config_name)
|
| 61 |
+
if path is None:
|
| 62 |
+
return None
|
| 63 |
+
text = path.read_text(encoding="utf-8")
|
| 64 |
+
m = re.search(r"zero_stage\s*:\s*(\d+)", text, re.IGNORECASE)
|
| 65 |
+
if m:
|
| 66 |
+
return int(m.group(1))
|
| 67 |
+
m = re.search(r"deepspeed_config_file\s*:\s*(\S+)", text, re.IGNORECASE)
|
| 68 |
+
if not m:
|
| 69 |
+
return None
|
| 70 |
+
json_path = Path(m.group(1).strip().strip("'\""))
|
| 71 |
+
if not json_path.is_file():
|
| 72 |
+
json_path = _project_root() / json_path
|
| 73 |
+
if not json_path.is_file():
|
| 74 |
+
return None
|
| 75 |
+
ds_json = json.loads(json_path.read_text(encoding="utf-8"))
|
| 76 |
+
stage = (ds_json.get("zero_optimization") or {}).get("stage")
|
| 77 |
+
return int(stage) if stage is not None else None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def should_colocate_teacher_with_student(device_map: Optional[str] = None) -> bool:
|
| 81 |
+
"""True when frozen teacher should sit on the same GPU as the trainable student."""
|
| 82 |
+
raw = (device_map or os.environ.get("DYME_TEACHER_DEVICE_MAP", "")).strip().lower()
|
| 83 |
+
if raw in ("same", "colocate", "local"):
|
| 84 |
+
return True
|
| 85 |
+
if os.environ.get("DYME_DEEPSPEED_COLOCATE", "").strip().lower() in ("1", "true", "yes", "on"):
|
| 86 |
+
return True
|
| 87 |
+
if is_deepspeed_accelerate_config() and raw in ("", "auto"):
|
| 88 |
+
return True
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def gradient_checkpointing_enable_kwargs(config_name: Optional[str] = None) -> Optional[dict]:
|
| 93 |
+
"""
|
| 94 |
+
Kwargs for ``model.gradient_checkpointing_enable``.
|
| 95 |
+
|
| 96 |
+
DeepSpeed ZeRO-1/2 + reentrant checkpointing runs backward twice per segment and
|
| 97 |
+
hits: "parameter ... has already been reduced".
|
| 98 |
+
"""
|
| 99 |
+
if not is_deepspeed_accelerate_config(config_name):
|
| 100 |
+
return None
|
| 101 |
+
override = os.environ.get("DYME_GRADIENT_CHECKPOINTING_USE_REENTRANT", "").strip().lower()
|
| 102 |
+
if override in ("1", "true", "yes", "on"):
|
| 103 |
+
return {"use_reentrant": True}
|
| 104 |
+
if override in ("0", "false", "no", "off"):
|
| 105 |
+
return {"use_reentrant": False}
|
| 106 |
+
return {"use_reentrant": False}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def deepspeed_requires_single_student_forward(config_name: Optional[str] = None) -> bool:
|
| 110 |
+
"""
|
| 111 |
+
DeepSpeed ZeRO-1/2 cannot reduce gradients when the student runs multiple
|
| 112 |
+
forwards in one backward (GRPO micro-chunks + OPSD loop).
|
| 113 |
+
"""
|
| 114 |
+
stage = deepspeed_zero_stage(config_name)
|
| 115 |
+
return stage is not None and stage <= 2
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def should_disable_gradient_checkpointing(config_name: Optional[str] = None) -> bool:
|
| 119 |
+
"""Gradient checkpointing also triggers double reduction under ZeRO-1/2."""
|
| 120 |
+
return deepspeed_requires_single_student_forward(config_name)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def student_forward_chunk_size(
|
| 124 |
+
batch_size: int,
|
| 125 |
+
has_vision: bool,
|
| 126 |
+
config_name: Optional[str] = None,
|
| 127 |
+
) -> int:
|
| 128 |
+
"""
|
| 129 |
+
Micro-batch size for student forwards in ``_get_per_token_logps``.
|
| 130 |
+
|
| 131 |
+
Under ZeRO-1/2 we must use one forward per backward (full local batch by default).
|
| 132 |
+
Override with ``DYME_STUDENT_FORWARD_CHUNK`` only if you accept ZeRO-3+ or OOM risk.
|
| 133 |
+
"""
|
| 134 |
+
if not has_vision:
|
| 135 |
+
return batch_size
|
| 136 |
+
if not deepspeed_requires_single_student_forward(config_name):
|
| 137 |
+
return 1
|
| 138 |
+
override = os.environ.get("DYME_STUDENT_FORWARD_CHUNK", "").strip()
|
| 139 |
+
if override.isdigit():
|
| 140 |
+
return max(1, min(batch_size, int(override)))
|
| 141 |
+
return batch_size
|
opsd_utils/diagnostics.py
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Periodic full-detail diagnostics for weak reward / gradient signals."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
|
| 12 |
+
from opsd_utils import debug_log as opsd_debug
|
| 13 |
+
from opsd_utils.vocab_align import align_cross_model_logits
|
| 14 |
+
|
| 15 |
+
PAREN_TOKEN_ID = 340
|
| 16 |
+
|
| 17 |
+
# Reuse logits from the OPSD loss path instead of running extra forwards.
|
| 18 |
+
_OPSD_JSD_DETAIL_CAPTURE: dict[str, Any] = {
|
| 19 |
+
"active": False,
|
| 20 |
+
"global_step": None,
|
| 21 |
+
"target_indices": set(),
|
| 22 |
+
"entries": [],
|
| 23 |
+
"skipped_memory": False,
|
| 24 |
+
"skip_reason": "",
|
| 25 |
+
"max_samples": 2,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _detail_min_free_gib() -> float:
|
| 30 |
+
raw = os.environ.get("DYME_OPSD_DETAIL_MIN_FREE_GB", "").strip()
|
| 31 |
+
if not raw:
|
| 32 |
+
return 4.0
|
| 33 |
+
try:
|
| 34 |
+
return max(0.0, float(raw))
|
| 35 |
+
except ValueError:
|
| 36 |
+
return 4.0
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def cuda_free_gib(device: Optional[torch.device | int] = None) -> Optional[float]:
|
| 40 |
+
if not torch.cuda.is_available():
|
| 41 |
+
return None
|
| 42 |
+
try:
|
| 43 |
+
if device is None:
|
| 44 |
+
free_bytes, _ = torch.cuda.mem_get_info()
|
| 45 |
+
else:
|
| 46 |
+
dev = torch.device(device) if not isinstance(device, torch.device) else device
|
| 47 |
+
with torch.cuda.device(dev):
|
| 48 |
+
free_bytes, _ = torch.cuda.mem_get_info()
|
| 49 |
+
return free_bytes / (1024**3)
|
| 50 |
+
except Exception:
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def check_detail_cuda_memory(
|
| 55 |
+
min_free_gib: Optional[float] = None,
|
| 56 |
+
device: Optional[torch.device | int] = None,
|
| 57 |
+
) -> tuple[bool, str, Optional[float]]:
|
| 58 |
+
"""Return (ok, reason, free_gib). Skips heavy detail work when GPU headroom is low."""
|
| 59 |
+
threshold = _detail_min_free_gib() if min_free_gib is None else max(0.0, float(min_free_gib))
|
| 60 |
+
if not torch.cuda.is_available():
|
| 61 |
+
return True, "", None
|
| 62 |
+
free_gib = cuda_free_gib(device)
|
| 63 |
+
if free_gib is None:
|
| 64 |
+
return True, "", None
|
| 65 |
+
if free_gib < threshold:
|
| 66 |
+
return (
|
| 67 |
+
False,
|
| 68 |
+
f"cuda_free_gib={free_gib:.2f} < min_free_gib={threshold:.2f}",
|
| 69 |
+
free_gib,
|
| 70 |
+
)
|
| 71 |
+
return True, "", free_gib
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def begin_opsd_jsd_detail_capture(
|
| 75 |
+
global_step: int,
|
| 76 |
+
opsd_indices: list[int],
|
| 77 |
+
max_samples: int = 2,
|
| 78 |
+
) -> None:
|
| 79 |
+
"""Prepare to record JSD stats during OPSD loss (no extra model forwards)."""
|
| 80 |
+
_OPSD_JSD_DETAIL_CAPTURE.update(
|
| 81 |
+
active=False,
|
| 82 |
+
global_step=global_step,
|
| 83 |
+
target_indices=set(),
|
| 84 |
+
entries=[],
|
| 85 |
+
skipped_memory=False,
|
| 86 |
+
skip_reason="",
|
| 87 |
+
max_samples=max(1, int(max_samples)),
|
| 88 |
+
)
|
| 89 |
+
if not opsd_debug.should_log_detail(global_step) or not opsd_indices:
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
ok, reason, free_gib = check_detail_cuda_memory()
|
| 93 |
+
if not ok:
|
| 94 |
+
_OPSD_JSD_DETAIL_CAPTURE["skipped_memory"] = True
|
| 95 |
+
_OPSD_JSD_DETAIL_CAPTURE["skip_reason"] = reason
|
| 96 |
+
opsd_debug.log_detail(
|
| 97 |
+
"opsd_jsd",
|
| 98 |
+
"skip JSD detail capture (CUDA memory guard)",
|
| 99 |
+
global_step=global_step,
|
| 100 |
+
reason=reason,
|
| 101 |
+
cuda_free_gib=free_gib,
|
| 102 |
+
min_free_gib=_detail_min_free_gib(),
|
| 103 |
+
)
|
| 104 |
+
return
|
| 105 |
+
|
| 106 |
+
_OPSD_JSD_DETAIL_CAPTURE["active"] = True
|
| 107 |
+
_OPSD_JSD_DETAIL_CAPTURE["target_indices"] = set(opsd_indices[: _OPSD_JSD_DETAIL_CAPTURE["max_samples"]])
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def maybe_capture_opsd_jsd_detail(
|
| 111 |
+
*,
|
| 112 |
+
global_idx: int,
|
| 113 |
+
student_logits: torch.Tensor,
|
| 114 |
+
teacher_logits: torch.Tensor,
|
| 115 |
+
completion_mask: torch.Tensor,
|
| 116 |
+
completion_ids: torch.Tensor,
|
| 117 |
+
beta: float,
|
| 118 |
+
tokenizer: Any = None,
|
| 119 |
+
student_prompt_len: Optional[int] = None,
|
| 120 |
+
teacher_prompt_len: Optional[int] = None,
|
| 121 |
+
) -> None:
|
| 122 |
+
"""Record token-level JSD stats from logits already computed in the loss path."""
|
| 123 |
+
capture = _OPSD_JSD_DETAIL_CAPTURE
|
| 124 |
+
if not capture["active"] or global_idx not in capture["target_indices"]:
|
| 125 |
+
return
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
with torch.no_grad():
|
| 129 |
+
s_logits, t_logits = align_cross_model_logits(
|
| 130 |
+
student_logits.detach(),
|
| 131 |
+
teacher_logits.detach(),
|
| 132 |
+
)
|
| 133 |
+
stats = jsd_token_stats(s_logits, t_logits, completion_mask.float(), beta=beta)
|
| 134 |
+
stats["sample_index"] = global_idx
|
| 135 |
+
if student_prompt_len is not None:
|
| 136 |
+
stats["student_prompt_len"] = int(student_prompt_len)
|
| 137 |
+
if teacher_prompt_len is not None:
|
| 138 |
+
stats["teacher_prompt_len"] = int(teacher_prompt_len)
|
| 139 |
+
if tokenizer is not None:
|
| 140 |
+
decoded = tokenizer.decode(
|
| 141 |
+
completion_ids[0][completion_mask[0].bool()],
|
| 142 |
+
skip_special_tokens=True,
|
| 143 |
+
)
|
| 144 |
+
stats["completion_text"] = _preview_text(decoded)
|
| 145 |
+
capture["entries"].append(stats)
|
| 146 |
+
except RuntimeError as exc:
|
| 147 |
+
if "out of memory" in str(exc).lower():
|
| 148 |
+
if torch.cuda.is_available():
|
| 149 |
+
torch.cuda.empty_cache()
|
| 150 |
+
opsd_debug.log_detail(
|
| 151 |
+
"opsd_jsd",
|
| 152 |
+
f"skip JSD detail for sample[{global_idx}] (OOM during stats)",
|
| 153 |
+
global_step=capture.get("global_step"),
|
| 154 |
+
error=repr(exc),
|
| 155 |
+
)
|
| 156 |
+
return
|
| 157 |
+
raise
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _tensor_stats(t: torch.Tensor, name: str) -> dict[str, Any]:
|
| 161 |
+
if t is None or not isinstance(t, torch.Tensor) or t.numel() == 0:
|
| 162 |
+
return {name: "empty"}
|
| 163 |
+
with torch.no_grad():
|
| 164 |
+
flat = t.detach().float().reshape(-1)
|
| 165 |
+
return {
|
| 166 |
+
f"{name}/shape": tuple(t.shape),
|
| 167 |
+
f"{name}/mean": float(flat.mean().item()),
|
| 168 |
+
f"{name}/std": float(flat.std(unbiased=False).item()) if flat.numel() > 1 else 0.0,
|
| 169 |
+
f"{name}/min": float(flat.min().item()),
|
| 170 |
+
f"{name}/max": float(flat.max().item()),
|
| 171 |
+
f"{name}/abs_mean": float(flat.abs().mean().item()),
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _preview_text(text: str, max_len: int = 320) -> str:
|
| 176 |
+
text = (text or "").replace("\n", "\\n")
|
| 177 |
+
if len(text) > max_len:
|
| 178 |
+
return text[: max_len - 3] + "..."
|
| 179 |
+
return text or "<EMPTY>"
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _generation_config_summary(generation_config: Any) -> dict[str, Any]:
|
| 183 |
+
if generation_config is None:
|
| 184 |
+
return {}
|
| 185 |
+
keys = (
|
| 186 |
+
"max_new_tokens",
|
| 187 |
+
"do_sample",
|
| 188 |
+
"temperature",
|
| 189 |
+
"top_p",
|
| 190 |
+
"top_k",
|
| 191 |
+
"min_p",
|
| 192 |
+
"repetition_penalty",
|
| 193 |
+
"eos_token_id",
|
| 194 |
+
"pad_token_id",
|
| 195 |
+
"bos_token_id",
|
| 196 |
+
)
|
| 197 |
+
out: dict[str, Any] = {}
|
| 198 |
+
for k in keys:
|
| 199 |
+
if hasattr(generation_config, k):
|
| 200 |
+
v = getattr(generation_config, k)
|
| 201 |
+
out[k] = v.tolist() if isinstance(v, torch.Tensor) else v
|
| 202 |
+
return out
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _slice_generate_inputs(batch: dict[str, Any], index: int, batch_size: int) -> dict[str, Any]:
|
| 206 |
+
"""Take one row from batched generate/forward tensors (VLM-safe)."""
|
| 207 |
+
sliced: dict[str, Any] = {}
|
| 208 |
+
for key, value in batch.items():
|
| 209 |
+
if isinstance(value, torch.Tensor) and value.dim() >= 1 and value.size(0) == batch_size:
|
| 210 |
+
sliced[key] = value[index : index + 1]
|
| 211 |
+
else:
|
| 212 |
+
sliced[key] = value
|
| 213 |
+
return sliced
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _last_valid_prompt_positions(prompt_mask: torch.Tensor) -> torch.Tensor:
|
| 217 |
+
"""Return index of last valid (non-pad) prompt token per row; supports left padding."""
|
| 218 |
+
with torch.no_grad():
|
| 219 |
+
seq_len = prompt_mask.size(1)
|
| 220 |
+
positions = torch.arange(seq_len, device=prompt_mask.device).expand_as(prompt_mask)
|
| 221 |
+
masked = torch.where(prompt_mask.bool(), positions, torch.full_like(positions, -1))
|
| 222 |
+
return masked.max(dim=1).values
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def _max_same_token_run(token_ids: list[int]) -> tuple[int, Optional[int]]:
|
| 226 |
+
"""Longest run of identical consecutive token ids."""
|
| 227 |
+
if not token_ids:
|
| 228 |
+
return 0, None
|
| 229 |
+
best_run = 1
|
| 230 |
+
best_tok = token_ids[0]
|
| 231 |
+
run = 1
|
| 232 |
+
for i in range(1, len(token_ids)):
|
| 233 |
+
if token_ids[i] == token_ids[i - 1]:
|
| 234 |
+
run += 1
|
| 235 |
+
if run > best_run:
|
| 236 |
+
best_run = run
|
| 237 |
+
best_tok = token_ids[i]
|
| 238 |
+
else:
|
| 239 |
+
run = 1
|
| 240 |
+
return best_run, best_tok
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _detect_single_token_repeat(token_ids: list[int], min_run: int = 8) -> bool:
|
| 244 |
+
return _max_same_token_run(token_ids)[0] >= min_run
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _detect_char_repeat(text: str, min_run: int = 6) -> bool:
|
| 248 |
+
"""Detect consecutive repeated characters (CJK or ASCII), e.g. 其其其."""
|
| 249 |
+
if not text or len(text) < min_run:
|
| 250 |
+
return False
|
| 251 |
+
run = 1
|
| 252 |
+
for i in range(1, len(text)):
|
| 253 |
+
if text[i] == text[i - 1] and not text[i].isspace():
|
| 254 |
+
run += 1
|
| 255 |
+
if run >= min_run:
|
| 256 |
+
return True
|
| 257 |
+
else:
|
| 258 |
+
run = 1
|
| 259 |
+
return False
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def _count_char_repeat_samples(completions: Optional[list[str]], min_run: int = 6) -> int:
|
| 263 |
+
if not completions:
|
| 264 |
+
return 0
|
| 265 |
+
return sum(1 for c in completions if _detect_char_repeat(c or "", min_run=min_run))
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _detect_repeat_loop(token_ids: list[int], min_repeats: int = 4, ngram: int = 3) -> bool:
|
| 269 |
+
if len(token_ids) < ngram * min_repeats:
|
| 270 |
+
return False
|
| 271 |
+
last_start = len(token_ids) - ngram * min_repeats + 1
|
| 272 |
+
for start in range(max(0, last_start)):
|
| 273 |
+
gram = token_ids[start : start + ngram]
|
| 274 |
+
repeats = 1
|
| 275 |
+
pos = start + ngram
|
| 276 |
+
while pos + ngram <= len(token_ids) and token_ids[pos : pos + ngram] == gram:
|
| 277 |
+
repeats += 1
|
| 278 |
+
pos += ngram
|
| 279 |
+
if repeats >= min_repeats:
|
| 280 |
+
return True
|
| 281 |
+
return False
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _detect_degeneration(
|
| 285 |
+
token_ids: list[int],
|
| 286 |
+
text: str,
|
| 287 |
+
*,
|
| 288 |
+
answer_flag: str = "Answer:",
|
| 289 |
+
min_single_token_run: int = 8,
|
| 290 |
+
require_answer_flag: bool = True,
|
| 291 |
+
) -> tuple[bool, list[str]]:
|
| 292 |
+
"""Heuristics for repetitive / format-broken completions."""
|
| 293 |
+
reasons: list[str] = []
|
| 294 |
+
if _detect_single_token_repeat(token_ids, min_run=min_single_token_run):
|
| 295 |
+
run_len, tok = _max_same_token_run(token_ids)
|
| 296 |
+
reasons.append(f"SINGLE_TOKEN_REPEAT(run={run_len},tok={tok})")
|
| 297 |
+
if _detect_repeat_loop(token_ids):
|
| 298 |
+
reasons.append("NGRAM_REPEAT")
|
| 299 |
+
if len(token_ids) >= 16:
|
| 300 |
+
unique_ratio = len(set(token_ids)) / len(token_ids)
|
| 301 |
+
if unique_ratio < 0.12:
|
| 302 |
+
reasons.append(f"LOW_UNIQUE_RATIO({unique_ratio:.3f})")
|
| 303 |
+
if require_answer_flag:
|
| 304 |
+
answer_count = len(re.findall(f"(?i){re.escape(answer_flag)}", text or ""))
|
| 305 |
+
if answer_count != 1:
|
| 306 |
+
reasons.append(f"ANSWER_FLAG_COUNT({answer_count})")
|
| 307 |
+
if _detect_char_repeat(text or ""):
|
| 308 |
+
reasons.append("CHAR_REPEAT")
|
| 309 |
+
return bool(reasons), reasons
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _count_answer_flag(text: str, answer_flag: str = "Answer:") -> int:
|
| 313 |
+
return len(re.findall(f"(?i){re.escape(answer_flag)}", text or ""))
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def is_degenerate_completion(
|
| 317 |
+
token_ids: list[int],
|
| 318 |
+
text: str,
|
| 319 |
+
*,
|
| 320 |
+
answer_flag: str = "Answer:",
|
| 321 |
+
min_single_token_run: int = 8,
|
| 322 |
+
require_answer_flag: bool = True,
|
| 323 |
+
) -> bool:
|
| 324 |
+
"""Return True when completion looks like a repetition / format-broken sample."""
|
| 325 |
+
is_deg, _ = _detect_degeneration(
|
| 326 |
+
token_ids,
|
| 327 |
+
text,
|
| 328 |
+
answer_flag=answer_flag,
|
| 329 |
+
min_single_token_run=min_single_token_run,
|
| 330 |
+
require_answer_flag=require_answer_flag,
|
| 331 |
+
)
|
| 332 |
+
return is_deg
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def _count_paren_then_eos(
|
| 336 |
+
completion_ids: torch.Tensor,
|
| 337 |
+
completion_mask: torch.Tensor,
|
| 338 |
+
eos_id: Optional[int],
|
| 339 |
+
) -> int:
|
| 340 |
+
if eos_id is None or completion_ids.size(0) == 0:
|
| 341 |
+
return 0
|
| 342 |
+
count = 0
|
| 343 |
+
with torch.no_grad():
|
| 344 |
+
lengths = completion_mask.sum(dim=1)
|
| 345 |
+
for i in range(completion_ids.size(0)):
|
| 346 |
+
eff = int(lengths[i].item())
|
| 347 |
+
if eff <= 0:
|
| 348 |
+
continue
|
| 349 |
+
first = int(completion_ids[i, 0].item())
|
| 350 |
+
if first != PAREN_TOKEN_ID:
|
| 351 |
+
continue
|
| 352 |
+
if eff <= 2:
|
| 353 |
+
count += 1
|
| 354 |
+
elif eff >= 2 and int(completion_ids[i, 1].item()) == eos_id:
|
| 355 |
+
count += 1
|
| 356 |
+
return count
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def summarize_generate_probe_stats(
|
| 360 |
+
completion_ids: torch.Tensor,
|
| 361 |
+
completion_mask: torch.Tensor,
|
| 362 |
+
is_eos: torch.Tensor,
|
| 363 |
+
eos_id: Optional[int],
|
| 364 |
+
completions: Optional[list[str]] = None,
|
| 365 |
+
answer_flag: str = "Answer:",
|
| 366 |
+
max_completion_length: Optional[int] = None,
|
| 367 |
+
) -> dict[str, Any]:
|
| 368 |
+
with torch.no_grad():
|
| 369 |
+
lengths = completion_mask.sum(dim=1).float()
|
| 370 |
+
has_eos = is_eos.any(dim=1)
|
| 371 |
+
paren_then_eos = _count_paren_then_eos(completion_ids, completion_mask, eos_id)
|
| 372 |
+
repeat_loop = 0
|
| 373 |
+
degenerate_count = 0
|
| 374 |
+
max_run_lengths: list[float] = []
|
| 375 |
+
unique_ratios: list[float] = []
|
| 376 |
+
answer_flag_ok = 0
|
| 377 |
+
clipped_count = 0
|
| 378 |
+
degenerate_format_count = 0
|
| 379 |
+
degenerate_repeat_count = 0
|
| 380 |
+
format_without_thinking = 0
|
| 381 |
+
for i in range(completion_ids.size(0)):
|
| 382 |
+
eff = int(lengths[i].item())
|
| 383 |
+
if eff <= 0:
|
| 384 |
+
continue
|
| 385 |
+
ids = completion_ids[i, :eff].tolist()
|
| 386 |
+
text = completions[i] if completions and i < len(completions) else ""
|
| 387 |
+
if _detect_repeat_loop(ids) or _detect_single_token_repeat(ids):
|
| 388 |
+
repeat_loop += 1
|
| 389 |
+
run_len, _ = _max_same_token_run(ids)
|
| 390 |
+
max_run_lengths.append(float(run_len))
|
| 391 |
+
unique_ratios.append(len(set(ids)) / max(len(ids), 1))
|
| 392 |
+
if _count_answer_flag(text, answer_flag) == 1:
|
| 393 |
+
answer_flag_ok += 1
|
| 394 |
+
thinking = (text or "").lower().split(answer_flag.lower())[0]
|
| 395 |
+
if len(thinking.strip()) < 8:
|
| 396 |
+
format_without_thinking += 1
|
| 397 |
+
else:
|
| 398 |
+
degenerate_format_count += 1
|
| 399 |
+
if max_completion_length is not None and eff >= max_completion_length - 1:
|
| 400 |
+
clipped_count += 1
|
| 401 |
+
is_deg, reasons = _detect_degeneration(ids, text, answer_flag=answer_flag)
|
| 402 |
+
if is_deg:
|
| 403 |
+
degenerate_count += 1
|
| 404 |
+
non_flag = [r for r in reasons if not r.startswith("ANSWER_FLAG")]
|
| 405 |
+
if non_flag:
|
| 406 |
+
degenerate_repeat_count += 1
|
| 407 |
+
char_repeat_count = _count_char_repeat_samples(completions)
|
| 408 |
+
n = max(completion_ids.size(0), 1)
|
| 409 |
+
return {
|
| 410 |
+
"effective_tokens_mean": float(lengths.mean().item()),
|
| 411 |
+
"char_repeat_count": char_repeat_count,
|
| 412 |
+
"one_token_count": int((lengths == 1).sum().item()),
|
| 413 |
+
"paren_then_eos_count": paren_then_eos,
|
| 414 |
+
"repeat_loop_count": repeat_loop,
|
| 415 |
+
"eos_terminated_rate": float(has_eos.float().mean().item()),
|
| 416 |
+
"degenerate_count": degenerate_count,
|
| 417 |
+
"degenerate_rate": degenerate_count / n,
|
| 418 |
+
"degenerate_rate_format": degenerate_format_count / n,
|
| 419 |
+
"degenerate_rate_repeat": degenerate_repeat_count / n,
|
| 420 |
+
"format_without_thinking_rate": format_without_thinking / n,
|
| 421 |
+
"max_token_run_mean": float(sum(max_run_lengths) / len(max_run_lengths)) if max_run_lengths else 0.0,
|
| 422 |
+
"unique_token_ratio_mean": float(sum(unique_ratios) / len(unique_ratios)) if unique_ratios else 0.0,
|
| 423 |
+
"answer_flag_exactly_once_rate": answer_flag_ok / n,
|
| 424 |
+
"clipped_count": clipped_count,
|
| 425 |
+
"clipped_rate": clipped_count / n,
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def log_generate_context(
|
| 430 |
+
*,
|
| 431 |
+
global_step: int,
|
| 432 |
+
trainer_step: Optional[int],
|
| 433 |
+
generate_call_index: int,
|
| 434 |
+
model: Any,
|
| 435 |
+
model_wrapped: Any,
|
| 436 |
+
gradient_checkpointing: bool,
|
| 437 |
+
generation_config: Any,
|
| 438 |
+
is_fsdp_enabled: bool,
|
| 439 |
+
generate_runs_under_no_grad: bool,
|
| 440 |
+
) -> None:
|
| 441 |
+
if not opsd_debug.should_log_gendbg() or not opsd_debug.probe_log_model_context():
|
| 442 |
+
return
|
| 443 |
+
|
| 444 |
+
opsd_debug.set_detail_step(global_step)
|
| 445 |
+
dropout_in_train = sum(
|
| 446 |
+
1 for m in model.modules() if isinstance(m, nn.Dropout) and m.training
|
| 447 |
+
)
|
| 448 |
+
opsd_debug.log_gendbg(
|
| 449 |
+
"context",
|
| 450 |
+
"generate model context",
|
| 451 |
+
generate_call_index=generate_call_index,
|
| 452 |
+
trainer_step=trainer_step,
|
| 453 |
+
global_step=global_step,
|
| 454 |
+
model_training=bool(getattr(model, "training", None)),
|
| 455 |
+
model_wrapped_training=bool(getattr(model_wrapped, "training", None)),
|
| 456 |
+
gradient_checkpointing=gradient_checkpointing,
|
| 457 |
+
generation_use_cache=getattr(generation_config, "use_cache", None),
|
| 458 |
+
is_fsdp_enabled=is_fsdp_enabled,
|
| 459 |
+
generate_runs_under_no_grad=generate_runs_under_no_grad,
|
| 460 |
+
dropout_modules_in_train=dropout_in_train,
|
| 461 |
+
generation_config=_generation_config_summary(generation_config),
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def log_prompt_tail_probe(
|
| 466 |
+
*,
|
| 467 |
+
global_step: int,
|
| 468 |
+
trainer_step: Optional[int],
|
| 469 |
+
generate_call_index: int,
|
| 470 |
+
prompt_ids: torch.Tensor,
|
| 471 |
+
prompt_mask: torch.Tensor,
|
| 472 |
+
tokenizer: Any,
|
| 473 |
+
sample_count: int = 4,
|
| 474 |
+
tail_tokens: Optional[int] = None,
|
| 475 |
+
) -> None:
|
| 476 |
+
if not opsd_debug.should_log_gendbg():
|
| 477 |
+
return
|
| 478 |
+
|
| 479 |
+
opsd_debug.set_detail_step(global_step)
|
| 480 |
+
n_tail = tail_tokens if tail_tokens is not None else opsd_debug.probe_prompt_tail_tokens()
|
| 481 |
+
last_pos = _last_valid_prompt_positions(prompt_mask)
|
| 482 |
+
n = min(sample_count, prompt_ids.size(0))
|
| 483 |
+
|
| 484 |
+
for i in range(n):
|
| 485 |
+
end = int(last_pos[i].item()) + 1
|
| 486 |
+
start = max(0, end - n_tail)
|
| 487 |
+
tail_ids = prompt_ids[i, start:end].tolist()
|
| 488 |
+
eff_len = int(prompt_mask[i].sum().item())
|
| 489 |
+
tail_decode = tokenizer.decode(tail_ids, skip_special_tokens=False)
|
| 490 |
+
opsd_debug.log_gendbg(
|
| 491 |
+
"prompt_tail",
|
| 492 |
+
f"sample[{i}]",
|
| 493 |
+
generate_call_index=generate_call_index,
|
| 494 |
+
trainer_step=trainer_step,
|
| 495 |
+
prompt_effective_len=eff_len,
|
| 496 |
+
last_valid_idx=end - 1,
|
| 497 |
+
prompt_tail_token_ids=tail_ids,
|
| 498 |
+
prompt_tail_decode=_preview_text(tail_decode, 400),
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def summarize_first_token_logits_stats(
|
| 503 |
+
p_greedy_values: list[float],
|
| 504 |
+
p_eos_values: list[float],
|
| 505 |
+
entropy_values: list[float],
|
| 506 |
+
p_answer_values: Optional[list[float]] = None,
|
| 507 |
+
) -> dict[str, float]:
|
| 508 |
+
"""Aggregate first-token logit probe scalars across samples."""
|
| 509 |
+
def _mean(vals: list[float]) -> float:
|
| 510 |
+
return float(sum(vals) / len(vals)) if vals else 0.0
|
| 511 |
+
|
| 512 |
+
out = {
|
| 513 |
+
"p_greedy_first": _mean(p_greedy_values),
|
| 514 |
+
"p_eos_first": _mean(p_eos_values),
|
| 515 |
+
"entropy_first": _mean(entropy_values),
|
| 516 |
+
}
|
| 517 |
+
if p_answer_values:
|
| 518 |
+
out["p_answer_first"] = _mean(p_answer_values)
|
| 519 |
+
return out
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def answer_first_token_id(tokenizer: Any) -> Optional[int]:
|
| 523 |
+
for piece in ("Answer", "Answer:"):
|
| 524 |
+
ids = tokenizer.encode(piece, add_special_tokens=False)
|
| 525 |
+
if ids:
|
| 526 |
+
return int(ids[0])
|
| 527 |
+
return None
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def log_first_token_logits_probe(
|
| 531 |
+
*,
|
| 532 |
+
global_step: int,
|
| 533 |
+
trainer_step: Optional[int],
|
| 534 |
+
generate_call_index: int,
|
| 535 |
+
unwrapped_model: Any,
|
| 536 |
+
prompt_inputs_generate: dict[str, Any],
|
| 537 |
+
prompt_mask: torch.Tensor,
|
| 538 |
+
tokenizer: Any,
|
| 539 |
+
sample_count: int = 4,
|
| 540 |
+
) -> dict[str, Any]:
|
| 541 |
+
"""Forward once before generate; return greedy ids and aggregated logit stats."""
|
| 542 |
+
greedy_by_sample: dict[int, int] = {}
|
| 543 |
+
p_greedy_vals: list[float] = []
|
| 544 |
+
p_eos_vals: list[float] = []
|
| 545 |
+
entropy_vals: list[float] = []
|
| 546 |
+
p_answer_vals: list[float] = []
|
| 547 |
+
if not opsd_debug.should_log_gendbg() or not opsd_debug.probe_first_token_logits():
|
| 548 |
+
return {
|
| 549 |
+
"greedy_by_sample": greedy_by_sample,
|
| 550 |
+
**summarize_first_token_logits_stats([], [], [], []),
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
opsd_debug.set_detail_step(global_step)
|
| 554 |
+
eos_id = getattr(tokenizer, "eos_token_id", None)
|
| 555 |
+
answer_tid = answer_first_token_id(tokenizer)
|
| 556 |
+
forward_inputs = {k: v for k, v in prompt_inputs_generate.items() if k != "labels"}
|
| 557 |
+
batch_size = prompt_mask.size(0)
|
| 558 |
+
n = min(sample_count, batch_size)
|
| 559 |
+
last_pos = _last_valid_prompt_positions(prompt_mask)
|
| 560 |
+
|
| 561 |
+
for i in range(n):
|
| 562 |
+
try:
|
| 563 |
+
sample_inputs = _slice_generate_inputs(forward_inputs, i, batch_size)
|
| 564 |
+
with torch.no_grad():
|
| 565 |
+
outputs = unwrapped_model(**sample_inputs, use_cache=False)
|
| 566 |
+
logits = outputs.logits
|
| 567 |
+
pos = int(last_pos[i].item())
|
| 568 |
+
next_logits = logits[0, pos, :].float()
|
| 569 |
+
probs = F.softmax(next_logits, dim=-1)
|
| 570 |
+
greedy_id = int(next_logits.argmax().item())
|
| 571 |
+
greedy_by_sample[i] = greedy_id
|
| 572 |
+
entropy = float(-(probs * (probs + 1e-12).log()).sum().item())
|
| 573 |
+
fields: dict[str, Any] = {
|
| 574 |
+
"generate_call_index": generate_call_index,
|
| 575 |
+
"trainer_step": trainer_step,
|
| 576 |
+
"last_prompt_idx": pos,
|
| 577 |
+
"greedy_token_id": greedy_id,
|
| 578 |
+
"p_greedy": float(probs[greedy_id].item()),
|
| 579 |
+
"entropy": entropy,
|
| 580 |
+
"probe_mode": "per_sample_forward",
|
| 581 |
+
}
|
| 582 |
+
if eos_id is not None:
|
| 583 |
+
fields["p_eos"] = float(probs[eos_id].item())
|
| 584 |
+
if answer_tid is not None and answer_tid < probs.size(0):
|
| 585 |
+
fields["p_answer_first"] = float(probs[answer_tid].item())
|
| 586 |
+
p_answer_vals.append(fields["p_answer_first"])
|
| 587 |
+
if PAREN_TOKEN_ID < probs.size(0):
|
| 588 |
+
fields["p_token_340"] = float(probs[PAREN_TOKEN_ID].item())
|
| 589 |
+
topk = torch.topk(probs, k=min(5, probs.size(0)))
|
| 590 |
+
fields["top5"] = [
|
| 591 |
+
(int(tid), float(p)) for tid, p in zip(topk.indices.tolist(), topk.values.tolist())
|
| 592 |
+
]
|
| 593 |
+
p_greedy_vals.append(fields["p_greedy"])
|
| 594 |
+
entropy_vals.append(entropy)
|
| 595 |
+
if eos_id is not None:
|
| 596 |
+
p_eos_vals.append(fields["p_eos"])
|
| 597 |
+
opsd_debug.log_gendbg("first_token_logits", f"sample[{i}]", **fields)
|
| 598 |
+
except Exception as exc:
|
| 599 |
+
opsd_debug.log_gendbg(
|
| 600 |
+
"first_token_logits",
|
| 601 |
+
f"FAILED forward for sample[{i}]",
|
| 602 |
+
generate_call_index=generate_call_index,
|
| 603 |
+
error=repr(exc),
|
| 604 |
+
)
|
| 605 |
+
if torch.cuda.is_available():
|
| 606 |
+
torch.cuda.empty_cache()
|
| 607 |
+
logits_stats = summarize_first_token_logits_stats(
|
| 608 |
+
p_greedy_vals, p_eos_vals, entropy_vals, p_answer_vals
|
| 609 |
+
)
|
| 610 |
+
return {"greedy_by_sample": greedy_by_sample, **logits_stats}
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def log_first_token_logits_match(
|
| 614 |
+
*,
|
| 615 |
+
generate_call_index: int,
|
| 616 |
+
completion_ids: torch.Tensor,
|
| 617 |
+
greedy_by_sample: dict[int, int],
|
| 618 |
+
sample_count: int = 4,
|
| 619 |
+
) -> None:
|
| 620 |
+
"""Compare pre-generate greedy next token vs actual first generated token."""
|
| 621 |
+
if not opsd_debug.should_log_gendbg() or not opsd_debug.probe_first_token_logits():
|
| 622 |
+
return
|
| 623 |
+
|
| 624 |
+
n = min(sample_count, completion_ids.size(0))
|
| 625 |
+
for i in range(n):
|
| 626 |
+
if i not in greedy_by_sample or completion_ids.size(1) == 0:
|
| 627 |
+
continue
|
| 628 |
+
greedy_id = greedy_by_sample[i]
|
| 629 |
+
actual_id = int(completion_ids[i, 0].item())
|
| 630 |
+
opsd_debug.log_gendbg(
|
| 631 |
+
"first_token_match",
|
| 632 |
+
f"sample[{i}]",
|
| 633 |
+
generate_call_index=generate_call_index,
|
| 634 |
+
greedy_token_id=greedy_id,
|
| 635 |
+
actual_first_token=actual_id,
|
| 636 |
+
greedy_matches_actual=(greedy_id == actual_id),
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def log_generate_delta(
|
| 641 |
+
*,
|
| 642 |
+
generate_call_index: int,
|
| 643 |
+
current_stats: dict[str, Any],
|
| 644 |
+
previous_stats: Optional[dict[str, Any]],
|
| 645 |
+
) -> None:
|
| 646 |
+
if not opsd_debug.should_log_gendbg():
|
| 647 |
+
return
|
| 648 |
+
|
| 649 |
+
fields: dict[str, Any] = {
|
| 650 |
+
"generate_call_index": generate_call_index,
|
| 651 |
+
"current": current_stats,
|
| 652 |
+
}
|
| 653 |
+
if previous_stats is not None:
|
| 654 |
+
prev_idx = previous_stats.get("generate_call_index", generate_call_index - 1)
|
| 655 |
+
fields["prev_generate_call_index"] = prev_idx
|
| 656 |
+
for key in (
|
| 657 |
+
"effective_tokens_mean",
|
| 658 |
+
"one_token_count",
|
| 659 |
+
"paren_then_eos_count",
|
| 660 |
+
"repeat_loop_count",
|
| 661 |
+
"eos_terminated_rate",
|
| 662 |
+
"degenerate_count",
|
| 663 |
+
"degenerate_rate",
|
| 664 |
+
"clipped_rate",
|
| 665 |
+
"answer_flag_exactly_once_rate",
|
| 666 |
+
):
|
| 667 |
+
cur = current_stats.get(key)
|
| 668 |
+
prev = previous_stats.get(key)
|
| 669 |
+
if cur is not None and prev is not None:
|
| 670 |
+
if isinstance(cur, (int, float)) and isinstance(prev, (int, float)):
|
| 671 |
+
fields[f"delta_{key}"] = cur - prev
|
| 672 |
+
opsd_debug.log_gendbg("delta", "generate stats vs previous regenerate", **fields)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def log_generate_probe(
|
| 676 |
+
*,
|
| 677 |
+
global_step: int,
|
| 678 |
+
trainer_step: Optional[int],
|
| 679 |
+
prompt_length: int,
|
| 680 |
+
prompt_completion_ids: torch.Tensor,
|
| 681 |
+
completion_ids: torch.Tensor,
|
| 682 |
+
completion_mask: torch.Tensor,
|
| 683 |
+
is_eos: torch.Tensor,
|
| 684 |
+
eos_idx: torch.Tensor,
|
| 685 |
+
completions: list[str],
|
| 686 |
+
tokenizer: Any,
|
| 687 |
+
generation_config: Any,
|
| 688 |
+
max_completion_length: int,
|
| 689 |
+
num_generations: int,
|
| 690 |
+
sample_count: int = 4,
|
| 691 |
+
generate_call_index: Optional[int] = None,
|
| 692 |
+
answer_flag: str = "Answer:",
|
| 693 |
+
source: str = "generate",
|
| 694 |
+
) -> dict[str, Any]:
|
| 695 |
+
"""Emit [OPSD-PROBE] on every (re)generate — catches 1-token / empty completions early."""
|
| 696 |
+
stats = summarize_generate_probe_stats(
|
| 697 |
+
completion_ids,
|
| 698 |
+
completion_mask,
|
| 699 |
+
is_eos,
|
| 700 |
+
getattr(tokenizer, "eos_token_id", None),
|
| 701 |
+
completions=completions,
|
| 702 |
+
answer_flag=answer_flag,
|
| 703 |
+
max_completion_length=max_completion_length,
|
| 704 |
+
)
|
| 705 |
+
if generate_call_index is not None:
|
| 706 |
+
stats["generate_call_index"] = generate_call_index
|
| 707 |
+
|
| 708 |
+
if not opsd_debug.should_log_probe():
|
| 709 |
+
return stats
|
| 710 |
+
|
| 711 |
+
opsd_debug.set_detail_step(global_step)
|
| 712 |
+
tok = tokenizer
|
| 713 |
+
eos_id = getattr(tok, "eos_token_id", None)
|
| 714 |
+
pad_id = getattr(tok, "pad_token_id", None)
|
| 715 |
+
bos_id = getattr(tok, "bos_token_id", None)
|
| 716 |
+
|
| 717 |
+
with torch.no_grad():
|
| 718 |
+
lengths = completion_mask.sum(dim=1).float()
|
| 719 |
+
has_eos = is_eos.any(dim=1)
|
| 720 |
+
raw_gen_len = completion_ids.size(1)
|
| 721 |
+
|
| 722 |
+
opsd_debug.log_probe(
|
| 723 |
+
"generate",
|
| 724 |
+
"sft cold-start GT batch" if source == "sft_cold_start" else "raw generate summary",
|
| 725 |
+
trainer_step=trainer_step,
|
| 726 |
+
global_step=global_step,
|
| 727 |
+
source=source,
|
| 728 |
+
generate_call_index=generate_call_index,
|
| 729 |
+
prompt_length=prompt_length,
|
| 730 |
+
prompt_completion_shape=tuple(prompt_completion_ids.shape),
|
| 731 |
+
completion_ids_shape=tuple(completion_ids.shape),
|
| 732 |
+
raw_gen_tokens=raw_gen_len,
|
| 733 |
+
max_completion_length=max_completion_length,
|
| 734 |
+
num_generations=num_generations,
|
| 735 |
+
batch_size=completion_ids.size(0),
|
| 736 |
+
effective_tokens_mean=stats["effective_tokens_mean"],
|
| 737 |
+
effective_tokens_min=float(lengths.min().item()),
|
| 738 |
+
effective_tokens_max=float(lengths.max().item()),
|
| 739 |
+
zero_length_count=int((lengths == 0).sum().item()),
|
| 740 |
+
one_token_count=stats["one_token_count"],
|
| 741 |
+
paren_then_eos_count=stats["paren_then_eos_count"],
|
| 742 |
+
repeat_loop_count=stats["repeat_loop_count"],
|
| 743 |
+
eos_terminated_rate=stats["eos_terminated_rate"],
|
| 744 |
+
degenerate_count=stats["degenerate_count"],
|
| 745 |
+
degenerate_rate=stats["degenerate_rate"],
|
| 746 |
+
max_token_run_mean=stats["max_token_run_mean"],
|
| 747 |
+
unique_token_ratio_mean=stats["unique_token_ratio_mean"],
|
| 748 |
+
answer_flag_exactly_once_rate=stats["answer_flag_exactly_once_rate"],
|
| 749 |
+
clipped_count=stats["clipped_count"],
|
| 750 |
+
clipped_rate=stats["clipped_rate"],
|
| 751 |
+
tokenizer_eos_id=eos_id,
|
| 752 |
+
tokenizer_pad_id=pad_id,
|
| 753 |
+
tokenizer_bos_id=bos_id,
|
| 754 |
+
generation_config=_generation_config_summary(generation_config),
|
| 755 |
+
)
|
| 756 |
+
|
| 757 |
+
suspicious: list[str] = []
|
| 758 |
+
n = min(sample_count, len(completions))
|
| 759 |
+
for i in range(n):
|
| 760 |
+
eff = int(lengths[i].item())
|
| 761 |
+
eidx = int(eos_idx[i].item())
|
| 762 |
+
ids_head = completion_ids[i, : min(16, completion_ids.size(1))].tolist()
|
| 763 |
+
ids_all = completion_ids[i].tolist()
|
| 764 |
+
decode_skip = completions[i]
|
| 765 |
+
decode_keep = tok.decode(completion_ids[i], skip_special_tokens=False)
|
| 766 |
+
first_tok = int(completion_ids[i, 0].item()) if completion_ids.size(1) > 0 else None
|
| 767 |
+
first_is_eos = first_tok is not None and first_tok == eos_id
|
| 768 |
+
flags: list[str] = []
|
| 769 |
+
patterns: list[str] = []
|
| 770 |
+
if eff <= 0:
|
| 771 |
+
flags.append("ZERO_LEN")
|
| 772 |
+
if eff == 1:
|
| 773 |
+
flags.append("ONE_TOKEN")
|
| 774 |
+
if not (decode_skip or "").strip():
|
| 775 |
+
flags.append("EMPTY_DECODE")
|
| 776 |
+
if first_is_eos:
|
| 777 |
+
flags.append("FIRST_IS_EOS")
|
| 778 |
+
if eff <= 2 and first_tok == PAREN_TOKEN_ID:
|
| 779 |
+
patterns.append("PAREN_THEN_EOS")
|
| 780 |
+
if eff > 0:
|
| 781 |
+
ids_eff = completion_ids[i, :eff].tolist()
|
| 782 |
+
if _detect_repeat_loop(ids_eff):
|
| 783 |
+
patterns.append("REPEAT_LOOP")
|
| 784 |
+
if _detect_single_token_repeat(ids_eff):
|
| 785 |
+
run_len, repeat_tok_id = _max_same_token_run(ids_eff)
|
| 786 |
+
patterns.append(f"SINGLE_TOKEN_REPEAT({run_len}x{repeat_tok_id})")
|
| 787 |
+
is_deg, deg_reasons = _detect_degeneration(ids_eff, decode_skip, answer_flag=answer_flag)
|
| 788 |
+
if is_deg:
|
| 789 |
+
patterns.extend(deg_reasons)
|
| 790 |
+
if flags:
|
| 791 |
+
suspicious.append(f"sample[{i}]:{','.join(flags)}")
|
| 792 |
+
|
| 793 |
+
opsd_debug.log_probe(
|
| 794 |
+
"generate",
|
| 795 |
+
f"sample[{i}]",
|
| 796 |
+
group=i // num_generations if num_generations else 0,
|
| 797 |
+
effective_tokens=eff,
|
| 798 |
+
eos_idx=eidx,
|
| 799 |
+
has_eos=bool(has_eos[i].item()),
|
| 800 |
+
first_token_id=first_tok,
|
| 801 |
+
first_is_eos=first_is_eos,
|
| 802 |
+
token_ids_head=ids_head,
|
| 803 |
+
token_ids_all=ids_all if len(ids_all) <= 32 else ids_head + ["..."],
|
| 804 |
+
decode_skip_special=_preview_text(decode_skip, 400),
|
| 805 |
+
decode_keep_special=_preview_text(decode_keep, 400),
|
| 806 |
+
flags=flags or None,
|
| 807 |
+
patterns=patterns or None,
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
if suspicious:
|
| 811 |
+
opsd_debug.log_probe(
|
| 812 |
+
"generate",
|
| 813 |
+
"ALERT suspicious completions",
|
| 814 |
+
count=len(suspicious),
|
| 815 |
+
samples=suspicious,
|
| 816 |
+
hint="check eos_token_id, max_new_tokens, model.generate output, or training-time unwrap/FSDP",
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
if stats.get("degenerate_rate", 0) >= 0.25:
|
| 820 |
+
opsd_debug.log_probe(
|
| 821 |
+
"generate",
|
| 822 |
+
"ALERT repetition / format degeneration",
|
| 823 |
+
degenerate_count=stats["degenerate_count"],
|
| 824 |
+
degenerate_rate=stats["degenerate_rate"],
|
| 825 |
+
clipped_rate=stats["clipped_rate"],
|
| 826 |
+
answer_flag_exactly_once_rate=stats["answer_flag_exactly_once_rate"],
|
| 827 |
+
max_token_run_mean=stats["max_token_run_mean"],
|
| 828 |
+
hint="typical RL collapse: raise repetition_penalty, lower temperature, or shorten max_completion_length",
|
| 829 |
+
)
|
| 830 |
+
|
| 831 |
+
return stats
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
def log_cross_rank_generate_summary(
|
| 835 |
+
*,
|
| 836 |
+
accelerator: Any,
|
| 837 |
+
one_token_count: int,
|
| 838 |
+
effective_tokens_mean: float,
|
| 839 |
+
generate_call_index: int,
|
| 840 |
+
) -> None:
|
| 841 |
+
"""Gather per-rank generate stats; all ranks must call, log on rank 0 only."""
|
| 842 |
+
if not opsd_debug.probe_on_generate():
|
| 843 |
+
return
|
| 844 |
+
|
| 845 |
+
from accelerate.utils import gather as accel_gather
|
| 846 |
+
|
| 847 |
+
device = accelerator.device
|
| 848 |
+
world_size = accelerator.num_processes
|
| 849 |
+
local = torch.tensor(
|
| 850 |
+
[float(one_token_count), effective_tokens_mean],
|
| 851 |
+
dtype=torch.float32,
|
| 852 |
+
device=device,
|
| 853 |
+
)
|
| 854 |
+
# gather_for_metrics on 1D [2] concatenates ranks into [world_size*2]; use gather + view instead.
|
| 855 |
+
gathered = accel_gather(local.unsqueeze(0))
|
| 856 |
+
if not opsd_debug.should_log_gendbg():
|
| 857 |
+
return
|
| 858 |
+
|
| 859 |
+
if gathered.dim() == 1:
|
| 860 |
+
gathered = gathered.view(world_size, -1)
|
| 861 |
+
elif gathered.size(0) != world_size and gathered.numel() == world_size * 2:
|
| 862 |
+
gathered = gathered.reshape(world_size, 2)
|
| 863 |
+
one_tokens = gathered[:, 0].tolist()
|
| 864 |
+
means = gathered[:, 1].tolist()
|
| 865 |
+
opsd_debug.log_gendbg(
|
| 866 |
+
"cross_rank",
|
| 867 |
+
"generate summary across ranks",
|
| 868 |
+
generate_call_index=generate_call_index,
|
| 869 |
+
world_size=world_size,
|
| 870 |
+
one_token_count_per_rank=one_tokens,
|
| 871 |
+
effective_tokens_mean_per_rank=means,
|
| 872 |
+
one_token_count_total=int(sum(one_tokens)),
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
def log_routed_completion_probe(
|
| 877 |
+
*,
|
| 878 |
+
global_step: int,
|
| 879 |
+
trainer_step: Optional[int],
|
| 880 |
+
raw_completion_shape: tuple[int, ...],
|
| 881 |
+
final_completion_ids: torch.Tensor,
|
| 882 |
+
final_completion_mask: torch.Tensor,
|
| 883 |
+
opsd_mask_list: list[bool],
|
| 884 |
+
sample_count: int = 4,
|
| 885 |
+
tokenizer: Any,
|
| 886 |
+
sft_replaced_list: Optional[list[bool]] = None,
|
| 887 |
+
raw_completion_ids: Optional[torch.Tensor] = None,
|
| 888 |
+
) -> None:
|
| 889 |
+
"""Compare raw vs post-routing padded completion shapes (detect routing truncation)."""
|
| 890 |
+
if not opsd_debug.should_log_probe():
|
| 891 |
+
return
|
| 892 |
+
|
| 893 |
+
with torch.no_grad():
|
| 894 |
+
final_lengths = final_completion_mask.sum(dim=1).float()
|
| 895 |
+
|
| 896 |
+
opsd_debug.log_probe(
|
| 897 |
+
"route",
|
| 898 |
+
"post-routing completion shapes",
|
| 899 |
+
trainer_step=trainer_step,
|
| 900 |
+
global_step=global_step,
|
| 901 |
+
raw_completion_shape=raw_completion_shape,
|
| 902 |
+
final_completion_shape=tuple(final_completion_ids.shape),
|
| 903 |
+
final_mask_shape=tuple(final_completion_mask.shape),
|
| 904 |
+
final_effective_tokens_mean=float(final_lengths.mean().item()),
|
| 905 |
+
final_effective_tokens_min=float(final_lengths.min().item()),
|
| 906 |
+
final_effective_tokens_max=float(final_lengths.max().item()),
|
| 907 |
+
opsd_mask_true=sum(opsd_mask_list),
|
| 908 |
+
opsd_mask_false=len(opsd_mask_list) - sum(opsd_mask_list),
|
| 909 |
+
sft_replaced_count=sum(sft_replaced_list) if sft_replaced_list else None,
|
| 910 |
+
)
|
| 911 |
+
|
| 912 |
+
n = min(sample_count, final_completion_ids.size(0))
|
| 913 |
+
for i in range(n):
|
| 914 |
+
eff = int(final_lengths[i].item())
|
| 915 |
+
head = final_completion_ids[i, : min(12, final_completion_ids.size(1))].tolist()
|
| 916 |
+
text = tokenizer.decode(
|
| 917 |
+
final_completion_ids[i][final_completion_mask[i].bool()],
|
| 918 |
+
skip_special_tokens=True,
|
| 919 |
+
)
|
| 920 |
+
raw_head = None
|
| 921 |
+
raw_matches_final = None
|
| 922 |
+
if raw_completion_ids is not None and i < raw_completion_ids.size(0):
|
| 923 |
+
raw_eff = min(int(raw_completion_ids.size(1)), eff)
|
| 924 |
+
raw_head = raw_completion_ids[i, : min(12, raw_eff)].tolist()
|
| 925 |
+
raw_matches_final = raw_head == head if raw_head and head else None
|
| 926 |
+
opsd_debug.log_probe(
|
| 927 |
+
"route",
|
| 928 |
+
f"routed_sample[{i}]",
|
| 929 |
+
opsd_mask=opsd_mask_list[i] if i < len(opsd_mask_list) else None,
|
| 930 |
+
sft_replaced=sft_replaced_list[i] if sft_replaced_list and i < len(sft_replaced_list) else None,
|
| 931 |
+
effective_tokens=eff,
|
| 932 |
+
token_ids_head=head,
|
| 933 |
+
raw_token_ids_head=raw_head,
|
| 934 |
+
raw_matches_routed_head=raw_matches_final,
|
| 935 |
+
decode=_preview_text(text, 300),
|
| 936 |
+
)
|
| 937 |
+
|
| 938 |
+
|
| 939 |
+
def jsd_token_stats(
|
| 940 |
+
student_logits: torch.Tensor,
|
| 941 |
+
teacher_logits: torch.Tensor,
|
| 942 |
+
mask: torch.Tensor,
|
| 943 |
+
beta: float = 0.5,
|
| 944 |
+
top_k: int = 5,
|
| 945 |
+
) -> dict[str, Any]:
|
| 946 |
+
"""Token-level JSD breakdown without building the graph."""
|
| 947 |
+
with torch.no_grad():
|
| 948 |
+
student_log_probs = F.log_softmax(student_logits, dim=-1)
|
| 949 |
+
teacher_log_probs = F.log_softmax(teacher_logits, dim=-1)
|
| 950 |
+
student_probs = student_log_probs.exp()
|
| 951 |
+
teacher_probs = teacher_log_probs.exp()
|
| 952 |
+
|
| 953 |
+
if beta == 0:
|
| 954 |
+
jsd = F.kl_div(student_log_probs, teacher_log_probs, reduction="none", log_target=True)
|
| 955 |
+
elif beta == 1:
|
| 956 |
+
jsd = F.kl_div(teacher_log_probs, student_log_probs, reduction="none", log_target=True)
|
| 957 |
+
else:
|
| 958 |
+
beta_t = torch.tensor(beta, dtype=student_log_probs.dtype, device=student_log_probs.device)
|
| 959 |
+
mixture_log_probs = torch.logsumexp(
|
| 960 |
+
torch.stack([student_log_probs + torch.log1p(-beta_t), teacher_log_probs + torch.log(beta_t)]),
|
| 961 |
+
dim=0,
|
| 962 |
+
)
|
| 963 |
+
kl_teacher = F.kl_div(mixture_log_probs, teacher_log_probs, reduction="none", log_target=True)
|
| 964 |
+
kl_student = F.kl_div(mixture_log_probs, student_log_probs, reduction="none", log_target=True)
|
| 965 |
+
jsd = beta_t * kl_teacher + (1 - beta_t) * kl_student
|
| 966 |
+
|
| 967 |
+
per_token_jsd = jsd.sum(dim=-1)
|
| 968 |
+
m = mask.float()
|
| 969 |
+
valid = m.sum().clamp(min=1.0)
|
| 970 |
+
per_token_jsd_masked = per_token_jsd * m
|
| 971 |
+
|
| 972 |
+
# Top-1 agreement on completion tokens
|
| 973 |
+
s_top = student_logits.argmax(dim=-1)
|
| 974 |
+
t_top = teacher_logits.argmax(dim=-1)
|
| 975 |
+
agree = ((s_top == t_top).float() * m).sum() / valid
|
| 976 |
+
|
| 977 |
+
# Mean L2 distance of log-probs on gold completion tokens (if provided elsewhere)
|
| 978 |
+
logprob_l2 = ((student_log_probs - teacher_log_probs) ** 2).sum(dim=-1)
|
| 979 |
+
logprob_l2_masked = (logprob_l2 * m).sum() / valid
|
| 980 |
+
|
| 981 |
+
# Entropy gap
|
| 982 |
+
s_ent = -(student_probs * student_log_probs).sum(dim=-1)
|
| 983 |
+
t_ent = -(teacher_probs * teacher_log_probs).sum(dim=-1)
|
| 984 |
+
ent_gap = ((s_ent - t_ent).abs() * m).sum() / valid
|
| 985 |
+
|
| 986 |
+
stats: dict[str, Any] = {
|
| 987 |
+
"jsd_per_token_mean": float((per_token_jsd_masked.sum() / valid).item()),
|
| 988 |
+
"jsd_per_token_max": float(per_token_jsd[m.bool()].max().item()) if m.any() else 0.0,
|
| 989 |
+
"jsd_per_token_min": float(per_token_jsd[m.bool()].min().item()) if m.any() else 0.0,
|
| 990 |
+
"top1_agreement": float(agree.item()),
|
| 991 |
+
"logprob_l2_mean": float(logprob_l2_masked.item()),
|
| 992 |
+
"entropy_gap_mean": float(ent_gap.item()),
|
| 993 |
+
"mask_valid_tokens": int(valid.item()),
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
if m.any():
|
| 997 |
+
idx = int((per_token_jsd * m).argmax().item())
|
| 998 |
+
pos = idx % per_token_jsd.size(-1)
|
| 999 |
+
stats["max_jsd_token_pos"] = pos
|
| 1000 |
+
stats["max_jsd_value"] = float(per_token_jsd.reshape(-1)[idx].item())
|
| 1001 |
+
s_topk = torch.topk(student_probs[0, pos], k=min(top_k, student_probs.size(-1)))
|
| 1002 |
+
t_topk = torch.topk(teacher_probs[0, pos], k=min(top_k, teacher_probs.size(-1)))
|
| 1003 |
+
stats["student_topk_at_max_jsd"] = [
|
| 1004 |
+
(int(i), float(p)) for i, p in zip(s_topk.indices.tolist(), s_topk.values.tolist())
|
| 1005 |
+
]
|
| 1006 |
+
stats["teacher_topk_at_max_jsd"] = [
|
| 1007 |
+
(int(i), float(p)) for i, p in zip(t_topk.indices.tolist(), t_topk.values.tolist())
|
| 1008 |
+
]
|
| 1009 |
+
return stats
|
| 1010 |
+
|
| 1011 |
+
|
| 1012 |
+
def log_generation_diagnostics(
|
| 1013 |
+
*,
|
| 1014 |
+
global_step: int,
|
| 1015 |
+
completions: list[str],
|
| 1016 |
+
completion_ids: torch.Tensor,
|
| 1017 |
+
completion_mask: torch.Tensor,
|
| 1018 |
+
is_eos: torch.Tensor,
|
| 1019 |
+
max_completion_length: int,
|
| 1020 |
+
num_generations: int,
|
| 1021 |
+
sample_count: int = 4,
|
| 1022 |
+
) -> None:
|
| 1023 |
+
if not opsd_debug.should_log_detail(global_step):
|
| 1024 |
+
return
|
| 1025 |
+
|
| 1026 |
+
opsd_debug.log_detail_banner(global_step, "GENERATION & COMPLETION")
|
| 1027 |
+
|
| 1028 |
+
with torch.no_grad():
|
| 1029 |
+
lengths = completion_mask.sum(dim=1).float()
|
| 1030 |
+
has_eos = is_eos.any(dim=1)
|
| 1031 |
+
eos_rate = has_eos.float().mean().item()
|
| 1032 |
+
clipped = (~has_eos).float().mean().item()
|
| 1033 |
+
all_pad_or_zero = (lengths == 0).float().mean().item()
|
| 1034 |
+
at_max_len = (lengths >= max_completion_length - 1).float().mean().item()
|
| 1035 |
+
|
| 1036 |
+
fields: dict[str, Any] = {
|
| 1037 |
+
"batch_size": completion_ids.size(0),
|
| 1038 |
+
"num_generations": num_generations,
|
| 1039 |
+
"completion_max_len": completion_ids.size(1),
|
| 1040 |
+
"effective_tokens_mean": float(lengths.mean().item()),
|
| 1041 |
+
"effective_tokens_min": float(lengths.min().item()),
|
| 1042 |
+
"effective_tokens_max": float(lengths.max().item()),
|
| 1043 |
+
"eos_terminated_rate": eos_rate,
|
| 1044 |
+
"clipped_no_eos_rate": clipped,
|
| 1045 |
+
"zero_length_rate": all_pad_or_zero,
|
| 1046 |
+
"at_max_length_rate": at_max_len,
|
| 1047 |
+
}
|
| 1048 |
+
opsd_debug.log_detail("generation", "completion mask summary", **fields)
|
| 1049 |
+
|
| 1050 |
+
n = min(sample_count, len(completions))
|
| 1051 |
+
for i in range(n):
|
| 1052 |
+
gen_group = i // num_generations if num_generations else 0
|
| 1053 |
+
opsd_debug.log_detail(
|
| 1054 |
+
"generation",
|
| 1055 |
+
f"sample[{i}]",
|
| 1056 |
+
group=gen_group,
|
| 1057 |
+
effective_tokens=int(lengths[i].item()),
|
| 1058 |
+
has_eos=bool(has_eos[i].item()),
|
| 1059 |
+
text=_preview_text(completions[i]),
|
| 1060 |
+
raw_token_head=completion_ids[i, :12].tolist(),
|
| 1061 |
+
)
|
| 1062 |
+
|
| 1063 |
+
|
| 1064 |
+
def log_reward_diagnostics(
|
| 1065 |
+
*,
|
| 1066 |
+
global_step: int,
|
| 1067 |
+
format_rewards: torch.Tensor,
|
| 1068 |
+
acc_rewards: torch.Tensor,
|
| 1069 |
+
context_rewards: torch.Tensor,
|
| 1070 |
+
all_rewards: torch.Tensor,
|
| 1071 |
+
advantages: torch.Tensor,
|
| 1072 |
+
reward_weights: torch.Tensor,
|
| 1073 |
+
num_generations: int,
|
| 1074 |
+
answers: Optional[list[str]] = None,
|
| 1075 |
+
completions: Optional[list[str]] = None,
|
| 1076 |
+
sample_count: int = 4,
|
| 1077 |
+
) -> None:
|
| 1078 |
+
if not opsd_debug.should_log_detail(global_step):
|
| 1079 |
+
return
|
| 1080 |
+
|
| 1081 |
+
opsd_debug.log_detail_banner(global_step, "REWARD & ADVANTAGE")
|
| 1082 |
+
|
| 1083 |
+
w = reward_weights.detach().float()
|
| 1084 |
+
weighted = (
|
| 1085 |
+
format_rewards * w[0] + context_rewards * w[1] + acc_rewards * w[2]
|
| 1086 |
+
)
|
| 1087 |
+
|
| 1088 |
+
fields: dict[str, Any] = {
|
| 1089 |
+
"reward_weights": w.tolist(),
|
| 1090 |
+
"format_sum": float(format_rewards.sum().item()),
|
| 1091 |
+
"acc_sum": float(acc_rewards.sum().item()),
|
| 1092 |
+
"context_sum": float(context_rewards.sum().item()),
|
| 1093 |
+
"total_sum": float(all_rewards.sum().item()),
|
| 1094 |
+
"format_zero_rate": float((format_rewards == 0).float().mean().item()),
|
| 1095 |
+
"acc_zero_rate": float((acc_rewards == 0).float().mean().item()),
|
| 1096 |
+
"context_zero_rate": float((context_rewards == 0).float().mean().item()),
|
| 1097 |
+
"weighted_mean": float(weighted.mean().item()),
|
| 1098 |
+
"weighted_std": float(weighted.std(unbiased=False).item()) if weighted.numel() > 1 else 0.0,
|
| 1099 |
+
}
|
| 1100 |
+
adv_flat = advantages.reshape(-1)
|
| 1101 |
+
fields.update(
|
| 1102 |
+
{
|
| 1103 |
+
"advantage_mean": float(adv_flat.mean().item()),
|
| 1104 |
+
"advantage_std": float(adv_flat.std(unbiased=False).item()) if adv_flat.numel() > 1 else 0.0,
|
| 1105 |
+
"advantage_zero_rate": float((adv_flat.abs() < 1e-8).float().mean().item()),
|
| 1106 |
+
"advantage_min": float(adv_flat.min().item()),
|
| 1107 |
+
"advantage_max": float(adv_flat.max().item()),
|
| 1108 |
+
}
|
| 1109 |
+
)
|
| 1110 |
+
opsd_debug.log_detail("reward", "aggregate reward stats", **fields)
|
| 1111 |
+
|
| 1112 |
+
n = min(sample_count, format_rewards.numel())
|
| 1113 |
+
for i in range(n):
|
| 1114 |
+
g = i // num_generations if num_generations else 0
|
| 1115 |
+
extra: dict[str, Any] = {
|
| 1116 |
+
"group": g,
|
| 1117 |
+
"format": float(format_rewards.view(-1)[i].item()),
|
| 1118 |
+
"acc": float(acc_rewards.view(-1)[i].item()),
|
| 1119 |
+
"context": float(context_rewards.view(-1)[i].item()),
|
| 1120 |
+
"weighted": float(weighted.view(-1)[i].item()),
|
| 1121 |
+
"advantage": float(adv_flat.view(-1)[i].item()) if i < adv_flat.numel() else None,
|
| 1122 |
+
}
|
| 1123 |
+
if answers and i < len(answers):
|
| 1124 |
+
extra["gold_answer"] = _preview_text(str(answers[i // num_generations]), 80)
|
| 1125 |
+
if completions and i < len(completions):
|
| 1126 |
+
extra["completion"] = _preview_text(completions[i], 160)
|
| 1127 |
+
opsd_debug.log_detail("reward", f"per_sample[{i}]", **extra)
|
| 1128 |
+
|
| 1129 |
+
|
| 1130 |
+
def log_routing_diagnostics(
|
| 1131 |
+
*,
|
| 1132 |
+
global_step: int,
|
| 1133 |
+
opsd_active: bool,
|
| 1134 |
+
opsd_mask_list: list[bool],
|
| 1135 |
+
has_correct: torch.Tensor,
|
| 1136 |
+
completion_modes: Optional[list[int]] = None,
|
| 1137 |
+
recoverable_flags: Optional[list[bool]] = None,
|
| 1138 |
+
completion_advantages: Optional[torch.Tensor] = None,
|
| 1139 |
+
completion_mask: Optional[torch.Tensor] = None,
|
| 1140 |
+
) -> None:
|
| 1141 |
+
if not opsd_debug.should_log_detail(global_step):
|
| 1142 |
+
return
|
| 1143 |
+
|
| 1144 |
+
opsd_debug.log_detail_banner(global_step, "OPSD ROUTING & MASK")
|
| 1145 |
+
|
| 1146 |
+
opsd_true = sum(opsd_mask_list)
|
| 1147 |
+
fields: dict[str, Any] = {
|
| 1148 |
+
"opsd_active": opsd_active,
|
| 1149 |
+
"opsd_mask_true": opsd_true,
|
| 1150 |
+
"opsd_mask_false": len(opsd_mask_list) - opsd_true,
|
| 1151 |
+
"opsd_mask_ratio": opsd_true / max(len(opsd_mask_list), 1),
|
| 1152 |
+
"has_correct": has_correct.tolist() if hasattr(has_correct, "tolist") else has_correct,
|
| 1153 |
+
}
|
| 1154 |
+
if recoverable_flags is not None:
|
| 1155 |
+
fields["recoverable_flags"] = recoverable_flags
|
| 1156 |
+
if completion_modes is not None:
|
| 1157 |
+
from opsd_utils.debug_log import MODE_NAMES
|
| 1158 |
+
|
| 1159 |
+
counts = {MODE_NAMES.get(c, str(c)): completion_modes.count(c) for c in set(completion_modes)}
|
| 1160 |
+
fields["completion_mode_counts"] = counts
|
| 1161 |
+
|
| 1162 |
+
if completion_advantages is not None and completion_mask is not None:
|
| 1163 |
+
with torch.no_grad():
|
| 1164 |
+
pos = ((completion_advantages > 0) & (completion_mask > 0)).float().sum(dim=1)
|
| 1165 |
+
neg = ((completion_advantages < 0) & (completion_mask > 0)).float().sum(dim=1)
|
| 1166 |
+
zero_adv = ((completion_advantages.abs() < 1e-8) & (completion_mask > 0)).float().sum(dim=1)
|
| 1167 |
+
fields["adv_pos_tokens_mean"] = float(pos.mean().item())
|
| 1168 |
+
fields["adv_neg_tokens_mean"] = float(neg.mean().item())
|
| 1169 |
+
fields["adv_zero_tokens_mean"] = float(zero_adv.mean().item())
|
| 1170 |
+
|
| 1171 |
+
opsd_debug.log_detail("routing", "mode routing summary", **fields)
|
| 1172 |
+
|
| 1173 |
+
|
| 1174 |
+
def log_loss_diagnostics(
|
| 1175 |
+
*,
|
| 1176 |
+
global_step: int,
|
| 1177 |
+
grpo_loss: torch.Tensor,
|
| 1178 |
+
per_token_logps: torch.Tensor,
|
| 1179 |
+
old_per_token_logps: torch.Tensor,
|
| 1180 |
+
completion_mask: torch.Tensor,
|
| 1181 |
+
advantages: torch.Tensor,
|
| 1182 |
+
coef_1: torch.Tensor,
|
| 1183 |
+
per_token_loss: torch.Tensor,
|
| 1184 |
+
opsd_loss: Optional[torch.Tensor] = None,
|
| 1185 |
+
combined_loss: Optional[torch.Tensor] = None,
|
| 1186 |
+
opsd_mask: Optional[torch.Tensor] = None,
|
| 1187 |
+
epsilon_low: float = 0.2,
|
| 1188 |
+
epsilon_high: float = 0.2,
|
| 1189 |
+
) -> None:
|
| 1190 |
+
if not opsd_debug.should_log_detail(global_step):
|
| 1191 |
+
return
|
| 1192 |
+
|
| 1193 |
+
opsd_debug.log_detail_banner(global_step, "LOSS & GRADIENT SIGNAL")
|
| 1194 |
+
|
| 1195 |
+
with torch.no_grad():
|
| 1196 |
+
m = completion_mask.float()
|
| 1197 |
+
valid_per_sample = m.sum(dim=1).clamp(min=1.0)
|
| 1198 |
+
sample_grpo = (per_token_loss * m).sum(dim=1) / valid_per_sample
|
| 1199 |
+
|
| 1200 |
+
fields: dict[str, Any] = {
|
| 1201 |
+
"grpo_loss_scalar": float(grpo_loss.detach().item()),
|
| 1202 |
+
"grpo_per_sample_mean": float(sample_grpo.mean().item()),
|
| 1203 |
+
"grpo_per_sample_max": float(sample_grpo.max().item()),
|
| 1204 |
+
"completion_mask_tokens_mean": float(valid_per_sample.mean().item()),
|
| 1205 |
+
}
|
| 1206 |
+
fields.update(_tensor_stats(advantages, "advantages"))
|
| 1207 |
+
fields.update(_tensor_stats(per_token_logps, "per_token_logps"))
|
| 1208 |
+
fields.update(_tensor_stats(old_per_token_logps, "old_per_token_logps"))
|
| 1209 |
+
fields.update(_tensor_stats((per_token_logps - old_per_token_logps) * m, "logps_delta_masked"))
|
| 1210 |
+
fields.update(_tensor_stats(coef_1 * m, "coef_1_masked"))
|
| 1211 |
+
|
| 1212 |
+
low_clip = ((coef_1 < 1 - epsilon_low) & (advantages.unsqueeze(1) < 0) & (m > 0)).float().sum()
|
| 1213 |
+
high_clip = ((coef_1 > 1 + epsilon_high) & (advantages.unsqueeze(1) > 0) & (m > 0)).float().sum()
|
| 1214 |
+
fields["clipped_low_tokens"] = int(low_clip.item())
|
| 1215 |
+
fields["clipped_high_tokens"] = int(high_clip.item())
|
| 1216 |
+
fields["grpo_zero_loss_rate"] = float((sample_grpo.abs() < 1e-12).float().mean().item())
|
| 1217 |
+
|
| 1218 |
+
if opsd_loss is not None:
|
| 1219 |
+
fields["opsd_loss_scalar"] = float(opsd_loss.detach().item())
|
| 1220 |
+
if combined_loss is not None:
|
| 1221 |
+
fields["combined_loss_scalar"] = float(combined_loss.detach().item())
|
| 1222 |
+
if opsd_mask is not None:
|
| 1223 |
+
fields["opsd_batch_count"] = int(opsd_mask.sum().item())
|
| 1224 |
+
|
| 1225 |
+
# Weak-signal hints
|
| 1226 |
+
hints: list[str] = []
|
| 1227 |
+
if fields.get("advantages/abs_mean", 1.0) < 1e-6:
|
| 1228 |
+
hints.append("advantages≈0 → GRPO per-token loss≈0")
|
| 1229 |
+
if fields.get("grpo_zero_loss_rate", 0) > 0.9:
|
| 1230 |
+
hints.append("most samples have ~0 GRPO loss")
|
| 1231 |
+
if fields.get("completion_mask_tokens_mean", 0) < 2:
|
| 1232 |
+
hints.append("very few effective completion tokens in mask")
|
| 1233 |
+
if opsd_loss is not None and abs(fields.get("opsd_loss_scalar", 0)) < 1e-8:
|
| 1234 |
+
hints.append("OPSD JSD≈0 → student/teacher nearly identical on completion")
|
| 1235 |
+
fields["weak_signal_hints"] = hints or ["none"]
|
| 1236 |
+
|
| 1237 |
+
opsd_debug.log_detail("loss", "GRPO / OPSD loss breakdown", **fields)
|
| 1238 |
+
|
| 1239 |
+
|
| 1240 |
+
def summarize_loss_health(
|
| 1241 |
+
*,
|
| 1242 |
+
grpo_loss: torch.Tensor,
|
| 1243 |
+
per_token_logps: torch.Tensor,
|
| 1244 |
+
completion_mask: torch.Tensor,
|
| 1245 |
+
advantages: torch.Tensor,
|
| 1246 |
+
per_token_loss: torch.Tensor,
|
| 1247 |
+
opsd_loss: Optional[torch.Tensor] = None,
|
| 1248 |
+
combined_loss: Optional[torch.Tensor] = None,
|
| 1249 |
+
) -> dict[str, float]:
|
| 1250 |
+
"""Lightweight loss/signal summary for every-step health monitoring."""
|
| 1251 |
+
with torch.no_grad():
|
| 1252 |
+
m = completion_mask.float()
|
| 1253 |
+
valid_per_sample = m.sum(dim=1).clamp(min=1.0)
|
| 1254 |
+
sample_grpo = (per_token_loss * m).sum(dim=1) / valid_per_sample
|
| 1255 |
+
adv_flat = advantages.detach().float().reshape(-1)
|
| 1256 |
+
fields: dict[str, float] = {
|
| 1257 |
+
"grpo_loss_scalar": float(grpo_loss.detach().item()),
|
| 1258 |
+
"grpo_zero_loss_rate": float((sample_grpo.abs() < 1e-12).float().mean().item()),
|
| 1259 |
+
"advantages_abs_mean": float(adv_flat.abs().mean().item()) if adv_flat.numel() else 0.0,
|
| 1260 |
+
"completion_mask_tokens_mean": float(valid_per_sample.mean().item()),
|
| 1261 |
+
}
|
| 1262 |
+
if opsd_loss is not None:
|
| 1263 |
+
fields["opsd_loss_scalar"] = float(opsd_loss.detach().item())
|
| 1264 |
+
if combined_loss is not None:
|
| 1265 |
+
fields["combined_loss_scalar"] = float(combined_loss.detach().item())
|
| 1266 |
+
logps_delta = (per_token_logps * m).sum() / m.sum().clamp(min=1.0)
|
| 1267 |
+
fields["logps_delta_mean"] = float(logps_delta.item()) if m.sum() > 0 else 0.0
|
| 1268 |
+
return fields
|
| 1269 |
+
|
| 1270 |
+
|
| 1271 |
+
def summarize_batch_data_health(
|
| 1272 |
+
samples: list[dict[str, Any]],
|
| 1273 |
+
*,
|
| 1274 |
+
prompt_mask: Optional[torch.Tensor] = None,
|
| 1275 |
+
pixel_values: Optional[torch.Tensor] = None,
|
| 1276 |
+
) -> dict[str, Any]:
|
| 1277 |
+
"""Batch-level data I/O sanity for health monitoring."""
|
| 1278 |
+
n = max(len(samples), 1)
|
| 1279 |
+
vf_empty = 0
|
| 1280 |
+
prompt_lens: list[int] = []
|
| 1281 |
+
for sample in samples:
|
| 1282 |
+
vf = (
|
| 1283 |
+
sample.get("visual_fact_hint")
|
| 1284 |
+
or sample.get("visual_fact")
|
| 1285 |
+
or sample.get("visual_facts")
|
| 1286 |
+
or ""
|
| 1287 |
+
)
|
| 1288 |
+
if not str(vf).strip():
|
| 1289 |
+
vf_empty += 1
|
| 1290 |
+
if sample.get("prompt"):
|
| 1291 |
+
prompt_lens.append(len(str(sample["prompt"])))
|
| 1292 |
+
|
| 1293 |
+
out: dict[str, Any] = {
|
| 1294 |
+
"visual_fact_empty_rate": vf_empty / n,
|
| 1295 |
+
"batch_size": len(samples),
|
| 1296 |
+
"prompt_len_mean": float(sum(prompt_lens) / len(prompt_lens)) if prompt_lens else 0.0,
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
if prompt_mask is not None:
|
| 1300 |
+
with torch.no_grad():
|
| 1301 |
+
lengths = prompt_mask.sum(dim=1).float()
|
| 1302 |
+
out["prompt_tokens_mean"] = float(lengths.mean().item())
|
| 1303 |
+
out["prompt_tokens_max"] = float(lengths.max().item())
|
| 1304 |
+
|
| 1305 |
+
if pixel_values is not None and isinstance(pixel_values, torch.Tensor) and pixel_values.numel() > 0:
|
| 1306 |
+
with torch.no_grad():
|
| 1307 |
+
flat = pixel_values.detach().float().reshape(-1)
|
| 1308 |
+
out["pixel_mean"] = float(flat.mean().item())
|
| 1309 |
+
out["pixel_has_nan"] = bool(torch.isnan(flat).any().item())
|
| 1310 |
+
out["pixel_has_inf"] = bool(torch.isinf(flat).any().item())
|
| 1311 |
+
|
| 1312 |
+
return out
|
| 1313 |
+
|
| 1314 |
+
|
| 1315 |
+
def log_opsd_jsd_diagnostics(*, global_step: int) -> None:
|
| 1316 |
+
"""Emit cached JSD stats recorded during OPSD loss (zero extra model forwards)."""
|
| 1317 |
+
if not opsd_debug.should_log_detail(global_step):
|
| 1318 |
+
return
|
| 1319 |
+
|
| 1320 |
+
capture = _OPSD_JSD_DETAIL_CAPTURE
|
| 1321 |
+
if capture.get("global_step") != global_step:
|
| 1322 |
+
return
|
| 1323 |
+
|
| 1324 |
+
opsd_debug.log_detail_banner(global_step, "OPSD JSD DECOMPOSITION")
|
| 1325 |
+
|
| 1326 |
+
if capture.get("skipped_memory"):
|
| 1327 |
+
opsd_debug.log_detail(
|
| 1328 |
+
"opsd_jsd",
|
| 1329 |
+
"JSD detail skipped (CUDA memory guard)",
|
| 1330 |
+
global_step=global_step,
|
| 1331 |
+
reason=capture.get("skip_reason", ""),
|
| 1332 |
+
min_free_gib=_detail_min_free_gib(),
|
| 1333 |
+
cuda_free_gib=cuda_free_gib(),
|
| 1334 |
+
)
|
| 1335 |
+
_OPSD_JSD_DETAIL_CAPTURE["active"] = False
|
| 1336 |
+
return
|
| 1337 |
+
|
| 1338 |
+
entries = capture.get("entries") or []
|
| 1339 |
+
if not entries:
|
| 1340 |
+
opsd_debug.log_detail(
|
| 1341 |
+
"opsd_jsd",
|
| 1342 |
+
"no JSD detail captured (no OPSD samples on this rank or capture disabled)",
|
| 1343 |
+
global_step=global_step,
|
| 1344 |
+
)
|
| 1345 |
+
_OPSD_JSD_DETAIL_CAPTURE["active"] = False
|
| 1346 |
+
return
|
| 1347 |
+
|
| 1348 |
+
for k, stats in enumerate(entries):
|
| 1349 |
+
opsd_debug.log_detail("opsd_jsd", f"sample[{k}] token-level JSD", global_step=global_step, **stats)
|
| 1350 |
+
|
| 1351 |
+
_OPSD_JSD_DETAIL_CAPTURE["active"] = False
|
opsd_utils/leakage.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Detect OPSD / completion patterns that indicate privileged-information leakage."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
_LEAKAGE_PHRASES = (
|
| 8 |
+
"reference answer",
|
| 9 |
+
"reference reasoning",
|
| 10 |
+
"according to the reference",
|
| 11 |
+
"according to the answer",
|
| 12 |
+
"参考答案",
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
_ANSWER_IN_COMPLETION = re.compile(r"(?i)answer:\s*.+")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def completion_has_leakage_pattern(
|
| 19 |
+
text: str,
|
| 20 |
+
gold_answer: str | None = None,
|
| 21 |
+
*,
|
| 22 |
+
min_gold_substring_len: int = 4,
|
| 23 |
+
) -> bool:
|
| 24 |
+
"""Return True if completion text suggests privileged-info cheating."""
|
| 25 |
+
if not text or not text.strip():
|
| 26 |
+
return False
|
| 27 |
+
lower = text.lower()
|
| 28 |
+
for phrase in _LEAKAGE_PHRASES:
|
| 29 |
+
if phrase in lower:
|
| 30 |
+
return True
|
| 31 |
+
if gold_answer:
|
| 32 |
+
gold = gold_answer.strip()
|
| 33 |
+
if len(gold) >= min_gold_substring_len and gold.lower() in lower:
|
| 34 |
+
# Short numeric answers may false-positive; require Answer: prefix context
|
| 35 |
+
if _ANSWER_IN_COMPLETION.search(text):
|
| 36 |
+
return False
|
| 37 |
+
if len(gold) >= 8:
|
| 38 |
+
return True
|
| 39 |
+
return False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def privileged_suffix_has_gold(suffix: str, sample: dict[str, Any]) -> bool:
|
| 43 |
+
if not suffix.strip():
|
| 44 |
+
return False
|
| 45 |
+
if "[Reference Answer]" in suffix or "[Reference Reasoning]" in suffix:
|
| 46 |
+
return True
|
| 47 |
+
answer = (sample.get("answer") or "").strip()
|
| 48 |
+
hint = (sample.get("hint") or "").strip()
|
| 49 |
+
if answer and answer in suffix:
|
| 50 |
+
return True
|
| 51 |
+
if hint and len(hint) >= 8 and hint in suffix:
|
| 52 |
+
return True
|
| 53 |
+
return False
|
opsd_utils/opsd_loss.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
|
| 4 |
+
from opsd_utils import debug_log as opsd_debug
|
| 5 |
+
from opsd_utils import diagnostics as opsd_diagnostics
|
| 6 |
+
from opsd_utils.deepspeed_utils import deepspeed_requires_single_student_forward
|
| 7 |
+
from opsd_utils.teacher_batching import (
|
| 8 |
+
align_teacher_prompt_image_tokens,
|
| 9 |
+
as_batch_num_images_tensor,
|
| 10 |
+
get_teacher_vision_for_sample,
|
| 11 |
+
model_inference_device,
|
| 12 |
+
move_batch_num_images_to_model_device,
|
| 13 |
+
move_pixel_values_to_model_device,
|
| 14 |
+
student_batch_num_images_tensor,
|
| 15 |
+
)
|
| 16 |
+
from opsd_utils.vocab_align import align_cross_model_logits
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _slice_image_sizes(image_sizes, index: int):
|
| 20 |
+
"""Slice per-sample image_sizes for student path (one image per batch row)."""
|
| 21 |
+
if image_sizes is None:
|
| 22 |
+
return None
|
| 23 |
+
if isinstance(image_sizes, torch.Tensor):
|
| 24 |
+
if image_sizes.dim() == 0:
|
| 25 |
+
return image_sizes
|
| 26 |
+
return image_sizes[index : index + 1]
|
| 27 |
+
if isinstance(image_sizes, (list, tuple)):
|
| 28 |
+
return image_sizes[index]
|
| 29 |
+
return image_sizes
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _slice_image_sizes_batch(image_sizes, start: int, end: int):
|
| 33 |
+
"""Slice image_sizes for a micro-batch row range [start, end)."""
|
| 34 |
+
if image_sizes is None:
|
| 35 |
+
return None
|
| 36 |
+
if isinstance(image_sizes, torch.Tensor):
|
| 37 |
+
if image_sizes.dim() == 0:
|
| 38 |
+
return image_sizes
|
| 39 |
+
if image_sizes.shape[0] >= end:
|
| 40 |
+
return image_sizes[start:end]
|
| 41 |
+
return image_sizes
|
| 42 |
+
if isinstance(image_sizes, (list, tuple)):
|
| 43 |
+
return image_sizes[start:end] if len(image_sizes) >= end else image_sizes
|
| 44 |
+
return image_sizes
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _teacher_image_counts(inputs: dict, batch_size: int) -> list[int]:
|
| 48 |
+
"""Number of teacher images per batch sample (LLaVA-OV stacks images on dim 0)."""
|
| 49 |
+
counts = inputs.get("teacher_num_images")
|
| 50 |
+
if counts is None:
|
| 51 |
+
return [1] * batch_size
|
| 52 |
+
if isinstance(counts, torch.Tensor):
|
| 53 |
+
return [int(max(1, c)) for c in counts.detach().cpu().tolist()]
|
| 54 |
+
return [int(max(1, c)) for c in counts]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def slice_teacher_vision_inputs(
|
| 58 |
+
teacher_pixel_values,
|
| 59 |
+
teacher_image_sizes,
|
| 60 |
+
local: int,
|
| 61 |
+
num_images_per_sample: list[int],
|
| 62 |
+
):
|
| 63 |
+
"""
|
| 64 |
+
Slice teacher pixel_values / image_sizes for one batch sample.
|
| 65 |
+
LLaVA-OneVision uses dim-0 = total images across batch (not batch size).
|
| 66 |
+
"""
|
| 67 |
+
if teacher_pixel_values is None:
|
| 68 |
+
return None, None
|
| 69 |
+
start = sum(num_images_per_sample[:local])
|
| 70 |
+
end = start + num_images_per_sample[local]
|
| 71 |
+
t_pixel = teacher_pixel_values[start:end]
|
| 72 |
+
t_sizes = None
|
| 73 |
+
if teacher_image_sizes is not None and isinstance(teacher_image_sizes, torch.Tensor):
|
| 74 |
+
t_sizes = teacher_image_sizes[start:end]
|
| 75 |
+
return t_pixel, t_sizes
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def generalized_jsd_loss(student_logits, teacher_logits, mask, beta=0.5):
|
| 79 |
+
"""Token-level generalized JSD on completion positions."""
|
| 80 |
+
# Cross-model OPD: teacher logits already live on the teacher GPU; avoid
|
| 81 |
+
# copying them onto the student GPU (vocab × seq is multi-hundred MiB per sample).
|
| 82 |
+
jsd_device = teacher_logits.device
|
| 83 |
+
if student_logits.device != jsd_device:
|
| 84 |
+
student_logits = student_logits.to(jsd_device, non_blocking=True)
|
| 85 |
+
mask = mask.to(device=jsd_device, non_blocking=True)
|
| 86 |
+
|
| 87 |
+
comp_dtype = student_logits.dtype
|
| 88 |
+
if comp_dtype == torch.float32:
|
| 89 |
+
comp_dtype = torch.bfloat16
|
| 90 |
+
if student_logits.dtype != comp_dtype:
|
| 91 |
+
student_logits = student_logits.to(comp_dtype)
|
| 92 |
+
if teacher_logits.dtype != comp_dtype:
|
| 93 |
+
teacher_logits = teacher_logits.to(comp_dtype)
|
| 94 |
+
|
| 95 |
+
student_logits, teacher_logits = align_cross_model_logits(student_logits, teacher_logits)
|
| 96 |
+
student_log_probs = F.log_softmax(student_logits, dim=-1)
|
| 97 |
+
teacher_log_probs = F.log_softmax(teacher_logits, dim=-1)
|
| 98 |
+
opsd_debug.log(
|
| 99 |
+
"vocab_align",
|
| 100 |
+
"generalized_jsd_loss log_softmax on aligned vocab",
|
| 101 |
+
student_log_prob_shape=tuple(student_log_probs.shape),
|
| 102 |
+
teacher_log_prob_shape=tuple(teacher_log_probs.shape),
|
| 103 |
+
student_exp_sum=float(torch.exp(student_log_probs[0, 0]).sum().item()) if student_log_probs.numel() else None,
|
| 104 |
+
teacher_exp_sum=float(torch.exp(teacher_log_probs[0, 0]).sum().item()) if teacher_log_probs.numel() else None,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
if beta == 0:
|
| 108 |
+
jsd = F.kl_div(student_log_probs, teacher_log_probs, reduction="none", log_target=True)
|
| 109 |
+
elif beta == 1:
|
| 110 |
+
jsd = F.kl_div(teacher_log_probs, student_log_probs, reduction="none", log_target=True)
|
| 111 |
+
else:
|
| 112 |
+
beta_t = torch.tensor(beta, dtype=student_log_probs.dtype, device=student_log_probs.device)
|
| 113 |
+
mixture_log_probs = torch.logsumexp(
|
| 114 |
+
torch.stack([student_log_probs + torch.log1p(-beta_t), teacher_log_probs + torch.log(beta_t)]),
|
| 115 |
+
dim=0,
|
| 116 |
+
)
|
| 117 |
+
kl_teacher = F.kl_div(mixture_log_probs, teacher_log_probs, reduction="none", log_target=True)
|
| 118 |
+
kl_student = F.kl_div(mixture_log_probs, student_log_probs, reduction="none", log_target=True)
|
| 119 |
+
jsd = beta_t * kl_teacher + (1 - beta_t) * kl_student
|
| 120 |
+
|
| 121 |
+
jsd = jsd.sum(dim=-1)
|
| 122 |
+
jsd = jsd * mask
|
| 123 |
+
denom = mask.sum().clamp(min=1.0)
|
| 124 |
+
return jsd.sum() / denom
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _teacher_logits_with_oom_retry(
|
| 128 |
+
model,
|
| 129 |
+
processor,
|
| 130 |
+
teacher_prompt_ids,
|
| 131 |
+
teacher_prompt_mask,
|
| 132 |
+
completion_ids,
|
| 133 |
+
completion_mask,
|
| 134 |
+
t_pixel,
|
| 135 |
+
t_sizes,
|
| 136 |
+
logits_to_keep: int,
|
| 137 |
+
teacher_batch_num_images=None,
|
| 138 |
+
):
|
| 139 |
+
"""Teacher forward with OOM micro-batch halving (decision E). Batch dim is already 1 in OPSD loop."""
|
| 140 |
+
if processor is not None:
|
| 141 |
+
teacher_prompt_ids, teacher_prompt_mask = align_teacher_prompt_image_tokens(
|
| 142 |
+
model,
|
| 143 |
+
processor,
|
| 144 |
+
teacher_prompt_ids,
|
| 145 |
+
teacher_prompt_mask,
|
| 146 |
+
t_pixel,
|
| 147 |
+
t_sizes,
|
| 148 |
+
batch_num_images=teacher_batch_num_images,
|
| 149 |
+
)
|
| 150 |
+
teacher_device = model_inference_device(model)
|
| 151 |
+
teacher_prompt_ids = teacher_prompt_ids.to(teacher_device)
|
| 152 |
+
teacher_prompt_mask = teacher_prompt_mask.to(teacher_device)
|
| 153 |
+
completion_ids = completion_ids.to(teacher_device)
|
| 154 |
+
completion_mask = completion_mask.to(teacher_device)
|
| 155 |
+
t_pixel = move_pixel_values_to_model_device(model, t_pixel)
|
| 156 |
+
teacher_batch_num_images = move_batch_num_images_to_model_device(model, teacher_batch_num_images)
|
| 157 |
+
teacher_input = torch.cat([teacher_prompt_ids, completion_ids], dim=1)
|
| 158 |
+
teacher_attn = torch.cat([teacher_prompt_mask, completion_mask], dim=1)
|
| 159 |
+
oom_retries = 0
|
| 160 |
+
while True:
|
| 161 |
+
try:
|
| 162 |
+
with torch.no_grad():
|
| 163 |
+
return model(
|
| 164 |
+
input_ids=teacher_input,
|
| 165 |
+
attention_mask=teacher_attn,
|
| 166 |
+
pixel_values=t_pixel,
|
| 167 |
+
image_sizes=t_sizes,
|
| 168 |
+
batch_num_images=teacher_batch_num_images,
|
| 169 |
+
).logits[:, -logits_to_keep - 1 : -1, :]
|
| 170 |
+
except RuntimeError as exc:
|
| 171 |
+
if "out of memory" not in str(exc).lower():
|
| 172 |
+
raise
|
| 173 |
+
oom_retries += 1
|
| 174 |
+
opsd_debug.log(
|
| 175 |
+
"teacher_forward_oom",
|
| 176 |
+
"teacher OPSD forward OOM, clearing cache and retrying",
|
| 177 |
+
micro_batch_size=teacher_input.shape[0],
|
| 178 |
+
oom_retries=oom_retries,
|
| 179 |
+
)
|
| 180 |
+
if torch.cuda.is_available():
|
| 181 |
+
torch.cuda.empty_cache()
|
| 182 |
+
if oom_retries >= 3:
|
| 183 |
+
raise
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def slice_student_completion_logits(full_logits: torch.Tensor, logits_to_keep: int) -> torch.Tensor:
|
| 187 |
+
"""Completion-token logits aligned with ``_get_per_token_logps`` / OPSD JSD."""
|
| 188 |
+
logits = full_logits[:, -logits_to_keep - 1 :, :]
|
| 189 |
+
logits = logits[:, :-1, :]
|
| 190 |
+
return logits[:, -logits_to_keep:, :]
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def compute_vlm_opsd_loss(
|
| 194 |
+
model,
|
| 195 |
+
student_prompt_ids,
|
| 196 |
+
student_prompt_mask,
|
| 197 |
+
student_pixel_values,
|
| 198 |
+
student_image_sizes,
|
| 199 |
+
teacher_prompt_ids,
|
| 200 |
+
teacher_prompt_mask,
|
| 201 |
+
teacher_pixel_values,
|
| 202 |
+
completion_ids,
|
| 203 |
+
completion_mask,
|
| 204 |
+
beta=0.5,
|
| 205 |
+
teacher_image_sizes=None,
|
| 206 |
+
processor=None,
|
| 207 |
+
teacher_batch_num_images=None,
|
| 208 |
+
teacher_model=None,
|
| 209 |
+
global_idx: int | None = None,
|
| 210 |
+
capture_jsd_detail: bool = False,
|
| 211 |
+
tokenizer=None,
|
| 212 |
+
student_logits=None,
|
| 213 |
+
) -> torch.Tensor:
|
| 214 |
+
"""
|
| 215 |
+
OPSD / OPD: student vs teacher prompt, shared student completion.
|
| 216 |
+
When teacher_model is set, cross-model OPD (e.g. frozen 7B teacher); else self-OPSD.
|
| 217 |
+
"""
|
| 218 |
+
teacher_model = teacher_model if teacher_model is not None else model
|
| 219 |
+
opsd_debug.log(
|
| 220 |
+
"opsd_loss",
|
| 221 |
+
"compute_vlm_opsd_loss enter",
|
| 222 |
+
beta=beta,
|
| 223 |
+
student_prompt_shape=tuple(student_prompt_ids.shape),
|
| 224 |
+
teacher_prompt_shape=tuple(teacher_prompt_ids.shape),
|
| 225 |
+
completion_shape=tuple(completion_ids.shape),
|
| 226 |
+
has_teacher_pixel_values=teacher_pixel_values is not None,
|
| 227 |
+
teacher_pixel_values_shape=(
|
| 228 |
+
tuple(teacher_pixel_values.shape) if teacher_pixel_values is not None else None
|
| 229 |
+
),
|
| 230 |
+
)
|
| 231 |
+
student_batch_num_images = student_batch_num_images_tensor(
|
| 232 |
+
student_pixel_values, student_prompt_ids.shape[0]
|
| 233 |
+
)
|
| 234 |
+
if processor is not None and student_pixel_values is not None:
|
| 235 |
+
student_prompt_ids, student_prompt_mask = align_teacher_prompt_image_tokens(
|
| 236 |
+
model,
|
| 237 |
+
processor,
|
| 238 |
+
student_prompt_ids,
|
| 239 |
+
student_prompt_mask,
|
| 240 |
+
student_pixel_values,
|
| 241 |
+
student_image_sizes,
|
| 242 |
+
batch_num_images=student_batch_num_images,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
student_input = torch.cat([student_prompt_ids, completion_ids], dim=1)
|
| 246 |
+
student_attn = torch.cat([student_prompt_mask, completion_mask], dim=1)
|
| 247 |
+
|
| 248 |
+
logits_to_keep = completion_ids.size(1)
|
| 249 |
+
|
| 250 |
+
if student_logits is None:
|
| 251 |
+
with opsd_debug.timed("opsd_loss", "student forward (grad)"):
|
| 252 |
+
student_logits = model(
|
| 253 |
+
input_ids=student_input,
|
| 254 |
+
attention_mask=student_attn,
|
| 255 |
+
pixel_values=student_pixel_values,
|
| 256 |
+
image_sizes=student_image_sizes,
|
| 257 |
+
batch_num_images=student_batch_num_images,
|
| 258 |
+
).logits[:, -logits_to_keep - 1 : -1, :]
|
| 259 |
+
else:
|
| 260 |
+
opsd_debug.log(
|
| 261 |
+
"opsd_loss",
|
| 262 |
+
"reuse GRPO student completion logits (DeepSpeed single-forward)",
|
| 263 |
+
student_logits_shape=tuple(student_logits.shape),
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
t_pixel = teacher_pixel_values if teacher_pixel_values is not None else student_pixel_values
|
| 267 |
+
t_sizes = teacher_image_sizes if teacher_image_sizes is not None else student_image_sizes
|
| 268 |
+
with opsd_debug.timed("opsd_loss", "teacher forward (no grad)"):
|
| 269 |
+
teacher_logits = _teacher_logits_with_oom_retry(
|
| 270 |
+
teacher_model,
|
| 271 |
+
processor,
|
| 272 |
+
teacher_prompt_ids,
|
| 273 |
+
teacher_prompt_mask,
|
| 274 |
+
completion_ids,
|
| 275 |
+
completion_mask,
|
| 276 |
+
t_pixel,
|
| 277 |
+
t_sizes,
|
| 278 |
+
logits_to_keep,
|
| 279 |
+
teacher_batch_num_images=teacher_batch_num_images,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
cross_model = teacher_model is not model
|
| 283 |
+
if cross_model:
|
| 284 |
+
opsd_debug.log(
|
| 285 |
+
"opsd_loss",
|
| 286 |
+
"cross-model OPD logits",
|
| 287 |
+
student_vocab=student_logits.size(-1),
|
| 288 |
+
teacher_vocab=teacher_logits.size(-1),
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
loss = generalized_jsd_loss(student_logits, teacher_logits, completion_mask.float(), beta=beta)
|
| 292 |
+
|
| 293 |
+
if capture_jsd_detail and global_idx is not None:
|
| 294 |
+
opsd_diagnostics.maybe_capture_opsd_jsd_detail(
|
| 295 |
+
global_idx=global_idx,
|
| 296 |
+
student_logits=student_logits,
|
| 297 |
+
teacher_logits=teacher_logits,
|
| 298 |
+
completion_mask=completion_mask,
|
| 299 |
+
completion_ids=completion_ids,
|
| 300 |
+
beta=beta,
|
| 301 |
+
tokenizer=tokenizer,
|
| 302 |
+
student_prompt_len=int(student_prompt_mask.sum().item()),
|
| 303 |
+
teacher_prompt_len=int(teacher_prompt_mask.sum().item()),
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
del teacher_logits
|
| 307 |
+
if cross_model and torch.cuda.is_available():
|
| 308 |
+
torch.cuda.empty_cache()
|
| 309 |
+
|
| 310 |
+
opsd_debug.log("opsd_loss", "compute_vlm_opsd_loss done", loss=float(loss.detach().item()))
|
| 311 |
+
return loss
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def compute_vlm_opsd_loss_masked_batch(
|
| 315 |
+
model,
|
| 316 |
+
opsd_indices: list[int],
|
| 317 |
+
all_indices: list[int],
|
| 318 |
+
inputs: dict,
|
| 319 |
+
beta: float = 0.5,
|
| 320 |
+
processor=None,
|
| 321 |
+
teacher_model=None,
|
| 322 |
+
acc_gate: bool = True,
|
| 323 |
+
pad_to_count: int | None = None,
|
| 324 |
+
global_step: int | None = None,
|
| 325 |
+
tokenizer=None,
|
| 326 |
+
detail_max_samples: int = 2,
|
| 327 |
+
student_completion_logits=None,
|
| 328 |
+
) -> torch.Tensor:
|
| 329 |
+
"""Compute mean OPSD loss over opsd_indices within a batch.
|
| 330 |
+
|
| 331 |
+
Under DDP every rank must run the *same* number of student/teacher
|
| 332 |
+
forwards, otherwise the per-forward buffer broadcast (and gradient
|
| 333 |
+
reduction) collectives desync across ranks and NCCL eventually times out.
|
| 334 |
+
``pad_to_count`` is the global-max OPSD sample count across ranks; ranks
|
| 335 |
+
with fewer (or zero) real samples run extra zero-weighted "dummy" forwards
|
| 336 |
+
on a valid local row so the collective sequence stays aligned.
|
| 337 |
+
"""
|
| 338 |
+
real_count = len(opsd_indices)
|
| 339 |
+
target_count = pad_to_count if pad_to_count is not None else real_count
|
| 340 |
+
if target_count <= 0:
|
| 341 |
+
opsd_debug.log("opsd_loss", "compute_vlm_opsd_loss_masked_batch skipped (no OPSD samples)")
|
| 342 |
+
return torch.tensor(0.0, device=inputs["prompt_ids"].device, requires_grad=True)
|
| 343 |
+
|
| 344 |
+
opsd_debug.log(
|
| 345 |
+
"opsd_loss",
|
| 346 |
+
"compute_vlm_opsd_loss_masked_batch enter",
|
| 347 |
+
opsd_indices=opsd_indices,
|
| 348 |
+
all_indices=all_indices,
|
| 349 |
+
beta=beta,
|
| 350 |
+
real_count=real_count,
|
| 351 |
+
target_count=target_count,
|
| 352 |
+
)
|
| 353 |
+
capture_jsd_detail = (
|
| 354 |
+
global_step is not None and opsd_debug.should_log_detail(global_step)
|
| 355 |
+
)
|
| 356 |
+
if capture_jsd_detail:
|
| 357 |
+
opsd_diagnostics.begin_opsd_jsd_detail_capture(
|
| 358 |
+
global_step,
|
| 359 |
+
opsd_indices,
|
| 360 |
+
max_samples=detail_max_samples,
|
| 361 |
+
)
|
| 362 |
+
losses = []
|
| 363 |
+
idx_map = {g: i for i, g in enumerate(all_indices)}
|
| 364 |
+
batch_size = inputs["prompt_ids"].shape[0]
|
| 365 |
+
teacher_img_counts = _teacher_image_counts(inputs, batch_size)
|
| 366 |
+
|
| 367 |
+
for step_idx in range(target_count):
|
| 368 |
+
is_real = step_idx < real_count
|
| 369 |
+
# Dummy iterations reuse the first available row so shapes stay valid;
|
| 370 |
+
# their contribution is zeroed out below.
|
| 371 |
+
global_idx = opsd_indices[step_idx] if is_real else all_indices[0]
|
| 372 |
+
local = idx_map[global_idx]
|
| 373 |
+
student_sizes = _slice_image_sizes(inputs.get("img_sizes"), local)
|
| 374 |
+
t_pixel, teacher_sizes = get_teacher_vision_for_sample(
|
| 375 |
+
inputs, local, teacher_img_counts
|
| 376 |
+
)
|
| 377 |
+
if t_pixel is None:
|
| 378 |
+
t_pixel = inputs["pixel_values"][local : local + 1]
|
| 379 |
+
teacher_sizes = student_sizes
|
| 380 |
+
opsd_debug.log(
|
| 381 |
+
"opsd_loss",
|
| 382 |
+
"compute sample OPSD loss",
|
| 383 |
+
global_idx=global_idx,
|
| 384 |
+
local_idx=local,
|
| 385 |
+
teacher_num_images=teacher_img_counts[local],
|
| 386 |
+
student_image_sizes=student_sizes,
|
| 387 |
+
teacher_image_sizes=teacher_sizes,
|
| 388 |
+
teacher_pixel_values_shape=tuple(t_pixel.shape) if t_pixel is not None else None,
|
| 389 |
+
)
|
| 390 |
+
n_img = teacher_img_counts[local]
|
| 391 |
+
teacher_batch_num_images = as_batch_num_images_tensor(n_img, t_pixel)
|
| 392 |
+
if not is_real and deepspeed_requires_single_student_forward():
|
| 393 |
+
# ZeRO-1/2: avoid extra student forwards (even loss*0 still backprops).
|
| 394 |
+
losses.append(torch.zeros((), device=inputs["prompt_ids"].device, requires_grad=True))
|
| 395 |
+
continue
|
| 396 |
+
precomputed_student_logits = None
|
| 397 |
+
if student_completion_logits is not None:
|
| 398 |
+
precomputed_student_logits = student_completion_logits[local : local + 1]
|
| 399 |
+
with opsd_debug.timed("opsd_loss", f"sample_opsd_loss idx={global_idx}"):
|
| 400 |
+
loss = compute_vlm_opsd_loss(
|
| 401 |
+
model,
|
| 402 |
+
inputs["prompt_ids"][local : local + 1],
|
| 403 |
+
inputs["prompt_mask"][local : local + 1],
|
| 404 |
+
inputs["pixel_values"][local : local + 1],
|
| 405 |
+
student_sizes,
|
| 406 |
+
inputs["teacher_prompt_ids"][local : local + 1],
|
| 407 |
+
inputs["teacher_prompt_mask"][local : local + 1],
|
| 408 |
+
t_pixel,
|
| 409 |
+
inputs["completion_ids"][local : local + 1],
|
| 410 |
+
inputs["completion_mask"][local : local + 1],
|
| 411 |
+
beta=beta,
|
| 412 |
+
teacher_image_sizes=teacher_sizes,
|
| 413 |
+
processor=processor,
|
| 414 |
+
teacher_batch_num_images=teacher_batch_num_images,
|
| 415 |
+
teacher_model=teacher_model,
|
| 416 |
+
global_idx=global_idx if is_real else None,
|
| 417 |
+
capture_jsd_detail=capture_jsd_detail and is_real,
|
| 418 |
+
tokenizer=tokenizer,
|
| 419 |
+
student_logits=precomputed_student_logits,
|
| 420 |
+
)
|
| 421 |
+
if not is_real:
|
| 422 |
+
# Keep the autograd graph / DDP collective alive but contribute nothing.
|
| 423 |
+
loss = loss * 0.0
|
| 424 |
+
elif acc_gate and "acc_rewards" in inputs:
|
| 425 |
+
acc_val = float(inputs["acc_rewards"][global_idx].item())
|
| 426 |
+
loss = loss * max(0.0, 1.0 - acc_val)
|
| 427 |
+
losses.append(loss)
|
| 428 |
+
|
| 429 |
+
# Mean over real samples only; dummy (zero-weighted) forwards keep the
|
| 430 |
+
# collective sequence aligned across ranks without skewing the loss scale.
|
| 431 |
+
mean_loss = torch.stack(losses).sum() / max(real_count, 1)
|
| 432 |
+
opsd_debug.log(
|
| 433 |
+
"opsd_loss",
|
| 434 |
+
"compute_vlm_opsd_loss_masked_batch done",
|
| 435 |
+
mean_loss=float(mean_loss.detach().item()),
|
| 436 |
+
real_count=real_count,
|
| 437 |
+
target_count=target_count,
|
| 438 |
+
)
|
| 439 |
+
return mean_loss
|
opsd_utils/privileged/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (707 Bytes). View file
|
|
|
opsd_utils/privileged/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (436 Bytes). View file
|
|
|
opsd_utils/privileged/__pycache__/base.cpython-310.pyc
ADDED
|
Binary file (1.08 kB). View file
|
|
|
opsd_utils/privileged/__pycache__/base.cpython-312.pyc
ADDED
|
Binary file (1.18 kB). View file
|
|
|
opsd_utils/privileged/__pycache__/image_utils.cpython-310.pyc
ADDED
|
Binary file (4.03 kB). View file
|
|
|
opsd_utils/privileged/__pycache__/providers.cpython-312.pyc
ADDED
|
Binary file (4.56 kB). View file
|
|
|
opsd_utils/privileged/__pycache__/registry.cpython-310.pyc
ADDED
|
Binary file (3.52 kB). View file
|
|
|
opsd_utils/privileged/__pycache__/registry.cpython-312.pyc
ADDED
|
Binary file (2.18 kB). View file
|
|
|
opsd_utils/privileged/debug_artifacts.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Save privileged teacher images to disk on detail steps."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
from opsd_utils import debug_log as opsd_debug
|
| 11 |
+
|
| 12 |
+
_saved_counts: dict[int, int] = {}
|
| 13 |
+
_output_dir: Optional[str] = None
|
| 14 |
+
_cfg: dict[str, Any] = {}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def configure(output_dir: Optional[str] = None, privileged_debug_cfg: Optional[dict[str, Any]] = None) -> None:
|
| 18 |
+
global _output_dir, _cfg
|
| 19 |
+
_output_dir = output_dir
|
| 20 |
+
_cfg = dict(privileged_debug_cfg or {})
|
| 21 |
+
_saved_counts.clear()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _image_subdir() -> str:
|
| 25 |
+
return _cfg.get("image_subdir", "logs/images")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def maybe_save_privileged_images(
|
| 29 |
+
global_step: Optional[int],
|
| 30 |
+
sample_idx: int,
|
| 31 |
+
full_img: Optional[Image.Image],
|
| 32 |
+
crop_img: Optional[Image.Image],
|
| 33 |
+
meta: Optional[dict[str, Any]] = None,
|
| 34 |
+
output_dir: Optional[str] = None,
|
| 35 |
+
privileged_debug_cfg: Optional[dict[str, Any]] = None,
|
| 36 |
+
) -> Optional[str]:
|
| 37 |
+
"""
|
| 38 |
+
Save teacher privileged images when should_log_detail(global_step) is true.
|
| 39 |
+
Returns base path prefix if saved, else None.
|
| 40 |
+
"""
|
| 41 |
+
if global_step is None:
|
| 42 |
+
return None
|
| 43 |
+
if not opsd_debug.should_log_detail(global_step):
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
cfg = privileged_debug_cfg if privileged_debug_cfg is not None else _cfg
|
| 47 |
+
if not cfg.get("save_images", True):
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
max_samples = int(cfg.get("max_samples_per_detail", 2))
|
| 51 |
+
count = _saved_counts.get(global_step, 0)
|
| 52 |
+
if count >= max_samples:
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
base_out = output_dir or _output_dir
|
| 56 |
+
if not base_out:
|
| 57 |
+
opsd_debug.log(
|
| 58 |
+
"privileged_debug",
|
| 59 |
+
"skip image save (no output_dir)",
|
| 60 |
+
global_step=global_step,
|
| 61 |
+
sample_idx=sample_idx,
|
| 62 |
+
)
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
subdir = os.path.join(base_out, _image_subdir() if cfg is _cfg else cfg.get("image_subdir", "logs/images"))
|
| 66 |
+
os.makedirs(subdir, exist_ok=True)
|
| 67 |
+
|
| 68 |
+
prefix = os.path.join(subdir, f"step_{int(global_step):06d}_idx_{sample_idx}")
|
| 69 |
+
saved_paths: list[str] = []
|
| 70 |
+
|
| 71 |
+
if full_img is not None:
|
| 72 |
+
full_path = f"{prefix}_full.png"
|
| 73 |
+
full_img.save(full_path)
|
| 74 |
+
saved_paths.append(full_path)
|
| 75 |
+
|
| 76 |
+
if crop_img is not None:
|
| 77 |
+
crop_path = f"{prefix}_crop.png"
|
| 78 |
+
crop_img.save(crop_path)
|
| 79 |
+
saved_paths.append(crop_path)
|
| 80 |
+
|
| 81 |
+
meta_path = f"{prefix}_meta.json"
|
| 82 |
+
meta_payload = dict(meta or {})
|
| 83 |
+
meta_payload.update(
|
| 84 |
+
{
|
| 85 |
+
"global_step": global_step,
|
| 86 |
+
"sample_idx": sample_idx,
|
| 87 |
+
"saved_paths": saved_paths,
|
| 88 |
+
}
|
| 89 |
+
)
|
| 90 |
+
with open(meta_path, "w", encoding="utf-8") as f:
|
| 91 |
+
json.dump(meta_payload, f, ensure_ascii=False, indent=2)
|
| 92 |
+
|
| 93 |
+
_saved_counts[global_step] = count + 1
|
| 94 |
+
opsd_debug.log_detail(
|
| 95 |
+
"privileged_debug",
|
| 96 |
+
"privileged images saved",
|
| 97 |
+
global_step=global_step,
|
| 98 |
+
sample_idx=sample_idx,
|
| 99 |
+
prefix=prefix,
|
| 100 |
+
saved_paths=saved_paths,
|
| 101 |
+
meta_path=meta_path,
|
| 102 |
+
**{k: v for k, v in (meta or {}).items() if k not in ("full_size", "crop_size")},
|
| 103 |
+
)
|
| 104 |
+
return prefix
|
opsd_utils/privileged/providers.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any
|
| 2 |
+
|
| 3 |
+
from PIL import Image
|
| 4 |
+
|
| 5 |
+
from data_utils.chart.deplot_pipeline import format_deplot_for_teacher, is_deplot_placeholder
|
| 6 |
+
from data_utils.privileged_schema import parse_visual_fact
|
| 7 |
+
from opsd_utils.privileged.base import PrivilegedContextProvider
|
| 8 |
+
from opsd_utils.privileged.image_utils import heuristic_crop_from_visual_fact, load_rgb
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
DEFAULT_FORMAT_ONLY_HINT = (
|
| 12 |
+
"Use the following structure in your response:\n"
|
| 13 |
+
"Goal: ...\nObservation: ...\nReasoning: ...\nAnswer: ..."
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class FormatOnlyProvider(PrivilegedContextProvider):
|
| 18 |
+
"""Structure hint only — no gold answer or reference reasoning (anti-leakage)."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, hint_text: str | None = None):
|
| 21 |
+
self._hint_text = (hint_text or DEFAULT_FORMAT_ONLY_HINT).strip()
|
| 22 |
+
|
| 23 |
+
def build_teacher_suffix(self, sample: dict[str, Any]) -> str:
|
| 24 |
+
return self._hint_text
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class TextProvider(PrivilegedContextProvider):
|
| 28 |
+
def __init__(self, include_gold: bool = True):
|
| 29 |
+
self.include_gold = include_gold
|
| 30 |
+
|
| 31 |
+
def build_teacher_suffix(self, sample: dict[str, Any]) -> str:
|
| 32 |
+
if not self.include_gold:
|
| 33 |
+
return ""
|
| 34 |
+
parts = []
|
| 35 |
+
hint = (sample.get("hint") or "").strip()
|
| 36 |
+
answer = (sample.get("answer") or "").strip()
|
| 37 |
+
if hint:
|
| 38 |
+
parts.append(f"[Reference Reasoning]\n{hint}")
|
| 39 |
+
if answer:
|
| 40 |
+
parts.append(f"[Reference Answer]\n{answer}")
|
| 41 |
+
return "\n\n".join(parts)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class VisualFactsProvider(PrivilegedContextProvider):
|
| 45 |
+
"""B1: raw JSON visual facts; F1+F2 merge hint and deplot sources."""
|
| 46 |
+
|
| 47 |
+
def _collect_visual_fact_parts(self, sample: dict[str, Any]) -> list[str]:
|
| 48 |
+
parts: list[str] = []
|
| 49 |
+
hint_vf = sample.get("visual_fact_hint")
|
| 50 |
+
if hint_vf:
|
| 51 |
+
text = parse_visual_fact(hint_vf)
|
| 52 |
+
if text:
|
| 53 |
+
parts.append(f"[Visual Facts - Hint]\n{text}")
|
| 54 |
+
|
| 55 |
+
deplot_vf = sample.get("visual_fact_deplot")
|
| 56 |
+
if deplot_vf and not is_deplot_placeholder(deplot_vf):
|
| 57 |
+
text = format_deplot_for_teacher(deplot_vf)
|
| 58 |
+
if text:
|
| 59 |
+
parts.append(f"[Visual Facts - DePlot]\n{text}")
|
| 60 |
+
|
| 61 |
+
primary = sample.get("visual_fact") or sample.get("visual_facts")
|
| 62 |
+
if primary and not (hint_vf or deplot_vf):
|
| 63 |
+
text = parse_visual_fact(primary)
|
| 64 |
+
if text:
|
| 65 |
+
parts.append(f"[Visual Facts]\n{text}")
|
| 66 |
+
elif primary and (hint_vf or deplot_vf):
|
| 67 |
+
text = parse_visual_fact(primary)
|
| 68 |
+
if text:
|
| 69 |
+
parts.append(f"[Visual Facts - Combined]\n{text}")
|
| 70 |
+
|
| 71 |
+
return parts
|
| 72 |
+
|
| 73 |
+
def build_teacher_suffix(self, sample: dict[str, Any]) -> str:
|
| 74 |
+
parts = self._collect_visual_fact_parts(sample)
|
| 75 |
+
return "\n\n".join(parts)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class CropProvider(PrivilegedContextProvider):
|
| 79 |
+
"""Returns evidence crop as second teacher image (dual-image path uses image_utils)."""
|
| 80 |
+
|
| 81 |
+
def build_teacher_suffix(self, sample: dict[str, Any]) -> str:
|
| 82 |
+
return ""
|
| 83 |
+
|
| 84 |
+
def build_teacher_images(self, sample: dict[str, Any], crop_cfg: dict[str, Any] | None = None) -> list[Image.Image]:
|
| 85 |
+
image = sample.get("image")
|
| 86 |
+
if image is None:
|
| 87 |
+
return []
|
| 88 |
+
full = load_rgb(image)
|
| 89 |
+
if full is None:
|
| 90 |
+
return []
|
| 91 |
+
crop, _, _ = heuristic_crop_from_visual_fact(full, sample, crop_cfg)
|
| 92 |
+
return [crop]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class HybridProvider(PrivilegedContextProvider):
|
| 96 |
+
def __init__(
|
| 97 |
+
self,
|
| 98 |
+
provider_names: list[str],
|
| 99 |
+
crop_cfg: dict[str, Any] | None = None,
|
| 100 |
+
*,
|
| 101 |
+
text_include_gold: bool = True,
|
| 102 |
+
format_only_hint: str | None = None,
|
| 103 |
+
):
|
| 104 |
+
self._providers: list[PrivilegedContextProvider] = []
|
| 105 |
+
self._crop_cfg = crop_cfg or {}
|
| 106 |
+
for name in provider_names:
|
| 107 |
+
if name == "text":
|
| 108 |
+
self._providers.append(TextProvider(include_gold=text_include_gold))
|
| 109 |
+
elif name == "format_only":
|
| 110 |
+
self._providers.append(FormatOnlyProvider(format_only_hint))
|
| 111 |
+
elif name == "visual_facts":
|
| 112 |
+
self._providers.append(VisualFactsProvider())
|
| 113 |
+
elif name == "crop":
|
| 114 |
+
self._providers.append(CropProvider())
|
| 115 |
+
|
| 116 |
+
def build_teacher_suffix(self, sample: dict[str, Any]) -> str:
|
| 117 |
+
chunks = [p.build_teacher_suffix(sample) for p in self._providers]
|
| 118 |
+
chunks = [c for c in chunks if c.strip()]
|
| 119 |
+
return "\n\n".join(chunks)
|
| 120 |
+
|
| 121 |
+
def build_teacher_images(self, sample: dict[str, Any]) -> list[Image.Image]:
|
| 122 |
+
for p in self._providers:
|
| 123 |
+
if isinstance(p, CropProvider):
|
| 124 |
+
imgs = p.build_teacher_images(sample, self._crop_cfg)
|
| 125 |
+
if imgs:
|
| 126 |
+
return imgs
|
| 127 |
+
return []
|