Add kaggle/train_grpo.py
Browse files- kaggle/train_grpo.py +57 -13
kaggle/train_grpo.py
CHANGED
|
@@ -202,6 +202,11 @@ _TOOL_CALL_RE = re.compile(
|
|
| 202 |
r"<tool_call>\s*(\{.*?\})\s*</tool_call>",
|
| 203 |
re.DOTALL,
|
| 204 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
|
| 207 |
def _env_tool_callables(env: Any) -> list[Callable[..., str]]:
|
|
@@ -214,22 +219,61 @@ def _env_tool_callables(env: Any) -> list[Callable[..., str]]:
|
|
| 214 |
return [getattr(env, name) for name in names]
|
| 215 |
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
def _parse_tool_calls(text: str) -> list[dict[str, Any]]:
|
|
|
|
| 218 |
calls: list[dict[str, Any]] = []
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
try:
|
| 221 |
-
obj = json.loads(
|
| 222 |
-
name = obj.get("name") or obj.get("function", {}).get("name")
|
| 223 |
-
args = obj.get("arguments") or obj.get("parameters") or obj.get("function", {}).get("arguments") or {}
|
| 224 |
-
if isinstance(args, str):
|
| 225 |
-
try:
|
| 226 |
-
args = json.loads(args)
|
| 227 |
-
except json.JSONDecodeError:
|
| 228 |
-
args = {"input": args}
|
| 229 |
-
if name:
|
| 230 |
-
calls.append({"name": str(name), "arguments": args if isinstance(args, dict) else {}})
|
| 231 |
except json.JSONDecodeError:
|
| 232 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
return calls
|
| 234 |
|
| 235 |
|
|
@@ -994,8 +1038,8 @@ def main(cfg: Cfg | None = None) -> None:
|
|
| 994 |
bf16=use_bf16,
|
| 995 |
fp16=use_fp16,
|
| 996 |
remove_unused_columns=False,
|
| 997 |
-
#
|
| 998 |
-
|
| 999 |
hub_model_id=cfg.hub_model_id,
|
| 1000 |
hub_token=os.environ["HF_TOKEN"],
|
| 1001 |
push_to_hub=False,
|
|
|
|
| 202 |
r"<tool_call>\s*(\{.*?\})\s*</tool_call>",
|
| 203 |
re.DOTALL,
|
| 204 |
)
|
| 205 |
+
_JSON_FENCE_RE = re.compile(r"```(?:json)?\s*(\{.*?\})\s*```", re.DOTALL)
|
| 206 |
+
_BARE_JSON_RE = re.compile(
|
| 207 |
+
r"\{\s*\"name\"\s*:\s*\"[^\"]+\"\s*,\s*\"arguments\"\s*:\s*\{.*?\}\s*\}",
|
| 208 |
+
re.DOTALL,
|
| 209 |
+
)
|
| 210 |
|
| 211 |
|
| 212 |
def _env_tool_callables(env: Any) -> list[Callable[..., str]]:
|
|
|
|
| 219 |
return [getattr(env, name) for name in names]
|
| 220 |
|
| 221 |
|
| 222 |
+
def _parse_one_tool_obj(obj: dict[str, Any]) -> dict[str, Any] | None:
|
| 223 |
+
name = obj.get("name") or obj.get("function", {}).get("name")
|
| 224 |
+
args = (
|
| 225 |
+
obj.get("arguments")
|
| 226 |
+
or obj.get("parameters")
|
| 227 |
+
or obj.get("function", {}).get("arguments")
|
| 228 |
+
or {}
|
| 229 |
+
)
|
| 230 |
+
if isinstance(args, str):
|
| 231 |
+
try:
|
| 232 |
+
args = json.loads(args)
|
| 233 |
+
except json.JSONDecodeError:
|
| 234 |
+
args = {"input": args}
|
| 235 |
+
if not name:
|
| 236 |
+
return None
|
| 237 |
+
return {"name": str(name), "arguments": args if isinstance(args, dict) else {}}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
def _parse_tool_calls(text: str) -> list[dict[str, Any]]:
|
| 241 |
+
"""Parse Qwen-style tool calls: <tool_call>, ```json fences, or bare {name,arguments}."""
|
| 242 |
calls: list[dict[str, Any]] = []
|
| 243 |
+
seen: set[str] = set()
|
| 244 |
+
raw = text or ""
|
| 245 |
+
|
| 246 |
+
candidates: list[str] = []
|
| 247 |
+
candidates.extend(m.group(1) for m in _TOOL_CALL_RE.finditer(raw))
|
| 248 |
+
candidates.extend(m.group(1) for m in _JSON_FENCE_RE.finditer(raw))
|
| 249 |
+
candidates.extend(m.group(0) for m in _BARE_JSON_RE.finditer(raw))
|
| 250 |
+
|
| 251 |
+
# Whole-string JSON object / list fallback
|
| 252 |
+
stripped = raw.strip()
|
| 253 |
+
if stripped.startswith("{") or stripped.startswith("["):
|
| 254 |
+
candidates.append(stripped)
|
| 255 |
+
|
| 256 |
+
for blob in candidates:
|
| 257 |
try:
|
| 258 |
+
obj = json.loads(blob)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
except json.JSONDecodeError:
|
| 260 |
continue
|
| 261 |
+
items = obj if isinstance(obj, list) else [obj]
|
| 262 |
+
for item in items:
|
| 263 |
+
if not isinstance(item, dict):
|
| 264 |
+
continue
|
| 265 |
+
# unwrap {"type":"function","function":{...}}
|
| 266 |
+
if "function" in item and isinstance(item["function"], dict):
|
| 267 |
+
parsed = _parse_one_tool_obj(item["function"])
|
| 268 |
+
else:
|
| 269 |
+
parsed = _parse_one_tool_obj(item)
|
| 270 |
+
if parsed is None:
|
| 271 |
+
continue
|
| 272 |
+
key = json.dumps(parsed, sort_keys=True)
|
| 273 |
+
if key in seen:
|
| 274 |
+
continue
|
| 275 |
+
seen.add(key)
|
| 276 |
+
calls.append(parsed)
|
| 277 |
return calls
|
| 278 |
|
| 279 |
|
|
|
|
| 1038 |
bf16=use_bf16,
|
| 1039 |
fp16=use_fp16,
|
| 1040 |
remove_unused_columns=False,
|
| 1041 |
+
# Do not set reward_weights here: env get_reward is appended after init,
|
| 1042 |
+
# so a fixed [0,1] length mismatches and crashes GRPOTrainer.
|
| 1043 |
hub_model_id=cfg.hub_model_id,
|
| 1044 |
hub_token=os.environ["HF_TOKEN"],
|
| 1045 |
push_to_hub=False,
|