Buckets:
| """Korean chain-of-thought generation — the one gap mixing cannot close. | |
| A scan of all 872,052 tool rows and every source in sft_dataset/merged.jsonl | |
| returned `KOREAN with balanced <think>: 0`. The model's thinking mode is | |
| therefore English-only: it would reason for English math and code and never for | |
| Korean, on a Korean-first model. No reweighting fixes that; the data has to be | |
| made. | |
| The design follows rollout.py's principle rather than the earlier "ask the model | |
| to author a record" approach: **verify against ground truth that already exists** | |
| instead of trusting the teacher. Seeds are real Korean questions that ship with | |
| their own answers, so a generation can be checked rather than hoped over: | |
| kmmlu_train 14,935 multiple choice, answer ends '정답: (B)' -> exact | |
| orca_math_korean 13,600 word problems with a numeric answer -> exact | |
| kopen_platypus 9,552 reasoning prose, no extractable answer -> unverified | |
| Only the first two can be graded. That is the point: a wrong Korean reasoning | |
| trace that reaches the right letter is still useful training signal, but one that | |
| reaches the wrong letter is actively harmful, and without seeds carrying answers | |
| there is no way to tell them apart. | |
| Teacher is deepseek-ai/DeepSeek-V4-Flash-0731 (local, 256 experts / 6 active). | |
| Two non-obvious requirements, both learned the hard way in this repo: | |
| kv_cache_dtype must be fp8 (DeepSeek-V4's MLA layout asserts otherwise), and | |
| vLLM strips the EOS token that the official parser asserts on -- re-append it, | |
| but only when generation stopped on EOS rather than on the length limit, or | |
| truncated reasoning gets smuggled in as complete. | |
| env -u PYTHONPATH /home/work/MKD-LLM/vllm_env/bin/python \ | |
| data/korean_cot/generate_ko_cot.py --model <path> --bake-off --n 100 | |
| env -u PYTHONPATH /home/work/MKD-LLM/vllm_env/bin/python \ | |
| data/korean_cot/generate_ko_cot.py --model <path> --target 25000 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| import random | |
| import re | |
| import sys | |
| import time | |
| from pathlib import Path | |
| REPO = Path(__file__).resolve().parent.parent.parent | |
| sys.path.insert(0, str(REPO / "data" / "toolcall")) | |
| import encoding_dsv4 as ENC # noqa: E402 | |
| MERGED = REPO / "data" / "sft_dataset" / "merged.jsonl" | |
| EOS = "<|end▁of▁sentence|>" | |
| THINKING = "thinking" | |
| HANGUL = re.compile("[가-힣]") | |
| # Seeds whose answers can be graded, and the share of the plan each gets. | |
| GRADED = {"kmmlu_train": 0.40, "orca_math_korean": 0.40} | |
| UNGRADED = {"kopen_platypus": 0.20} | |
| SEED_SOURCES = {**GRADED, **UNGRADED} | |
| SYSTEM = ( | |
| "당신은 한국어로 사고하고 한국어로 답변하는 전문가입니다.\n" | |
| "반드시 다음 규칙을 지키십시오:\n" | |
| "1. 추론 과정 전체를 한국어로 작성하십시오. 영어나 중국어를 사용하지 마십시오.\n" | |
| "2. 단계적으로 근거를 밝히며 생각하십시오.\n" | |
| "3. 추론이 끝나면 최종 답변을 한국어로 간결하게 제시하십시오.\n" | |
| "4. 객관식 문제라면 마지막 줄에 '정답: (X)' 형식으로 답을 쓰십시오.\n" | |
| "5. 계산 문제라면 마지막 줄에 '정답: <숫자>' 형식으로 답을 쓰십시오." | |
| ) | |
| CHOICE = re.compile(r"정답\s*[::]\s*\(?\s*([A-Ea-e])\s*\)?") | |
| NUMBER = re.compile(r"-?\d[\d,]*\.?\d*") | |
| def hangul_ratio(s: str) -> float: | |
| s = re.sub(r"\s+", "", s or "") | |
| return len(HANGUL.findall(s)) / max(len(s), 1) | |
| def grade_key(source: str, answer_text: str): | |
| """The ground-truth answer a generation will be checked against, or None.""" | |
| if source == "kmmlu_train": | |
| m = CHOICE.search(answer_text) | |
| return ("choice", m.group(1).upper()) if m else None | |
| if source == "orca_math_korean": | |
| nums = NUMBER.findall(answer_text) | |
| return ("number", nums[-1].replace(",", "")) if nums else None | |
| return None | |
| def check(kind_val, produced: str) -> bool: | |
| kind, truth = kind_val | |
| if kind == "choice": | |
| m = CHOICE.search(produced) | |
| return bool(m) and m.group(1).upper() == truth | |
| nums = NUMBER.findall(produced) | |
| if not nums: | |
| return False | |
| try: | |
| return abs(float(nums[-1].replace(",", "")) - float(truth)) < 1e-6 | |
| except ValueError: | |
| return nums[-1].replace(",", "") == truth | |
| def load_seeds(rng: random.Random, n_wanted: int): | |
| """Collect Korean questions that carry their own answers.""" | |
| pools: dict[str, list] = {s: [] for s in SEED_SOURCES} | |
| for line in open(MERGED, encoding="utf-8"): | |
| if '"lang": "ko"' not in line and '"lang":"ko"' not in line: | |
| continue | |
| r = json.loads(line) | |
| s = r.get("source") | |
| if s not in pools: | |
| continue | |
| msgs = r.get("messages") or [] | |
| q = next((m.get("content") for m in msgs if m.get("role") == "user"), None) | |
| a = next((m.get("content") for m in msgs if m.get("role") == "assistant"), "") | |
| if not q or hangul_ratio(q) < 0.10: | |
| continue | |
| key = grade_key(s, a or "") if s in GRADED else None | |
| if s in GRADED and key is None: | |
| continue # ungradable: no answer to check against | |
| pools[s].append((q, key, s)) | |
| for s, v in pools.items(): | |
| rng.shuffle(v) | |
| print(f" seeds {s:22s} {len(v):>7,}", flush=True) | |
| jobs = [] | |
| for s, share in SEED_SOURCES.items(): | |
| jobs += pools[s][: max(1, int(n_wanted * share))] | |
| rng.shuffle(jobs) | |
| return jobs | |
| def parse_completion(gen): | |
| """Official parser, with the two adjustments vLLM forces (see module docstring).""" | |
| text = gen.text | |
| if getattr(gen, "finish_reason", None) == "length": | |
| return None # truncated reasoning: reject, never repair | |
| if text.startswith("<think>"): | |
| text = text[len("<think>"):] | |
| if EOS not in text: | |
| text += EOS | |
| try: | |
| return ENC.parse_message_from_completion_text(text, thinking_mode=THINKING) | |
| except Exception: # noqa: BLE001 | |
| return None | |
| def build_record(parsed, q: str, key, source: str, idx: int): | |
| """Gate a generation, and shape it the way assemble.py expects.""" | |
| reasoning = (parsed.get("reasoning_content") or "").strip() | |
| answer = (parsed.get("content") or "").strip() | |
| if not reasoning or not answer: | |
| return None, "empty" | |
| # The whole purpose is Korean reasoning. A Korean answer wrapped around an | |
| # English thought process teaches the model to think in English. | |
| if hangul_ratio(reasoning) < 0.30: | |
| return None, "reasoning_not_korean" | |
| if hangul_ratio(answer) < 0.30: | |
| return None, "answer_not_korean" | |
| if len(reasoning) < 80: | |
| return None, "reasoning_too_short" | |
| if any(reasoning.count(reasoning[-n:]) >= 4 for n in (40, 80) if reasoning[-n:]): | |
| return None, "degenerate" | |
| if key is not None and not check(key, answer): | |
| return None, "wrong_answer" | |
| content = f"<think>\n{reasoning}\n</think>\n\n{answer}" | |
| if content.count("<think>") != content.count("</think>"): | |
| return None, "unbalanced_think" | |
| return { | |
| "id": f"ko_cot_{idx:07d}", | |
| "source": "mkd_korean_cot_v1", | |
| "lang": "ko", | |
| "category": "korean_cot_verified" if key is not None else "korean_cot", | |
| "license": "generated", | |
| "share_alike": False, | |
| "seed_source": source, | |
| "messages": [{"role": "user", "content": q}, | |
| {"role": "assistant", "content": content}], | |
| }, None | |
| def dedup(recs): | |
| seen, out = set(), [] | |
| for r in recs: | |
| h = hashlib.blake2b( | |
| re.sub(r"\s+", " ", r["messages"][0]["content"]).strip().lower().encode(), | |
| digest_size=8).digest() | |
| if h not in seen: | |
| seen.add(h) | |
| out.append(r) | |
| return out | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--model", required=True) | |
| ap.add_argument("--target", type=int, default=25000, help="verified records wanted") | |
| ap.add_argument("--bake-off", action="store_true") | |
| ap.add_argument("--n", type=int, default=100) | |
| ap.add_argument("--out", default="data/korean_cot/ko_cot.jsonl") | |
| ap.add_argument("--tp", type=int, default=4) | |
| ap.add_argument("--max-model-len", type=int, default=8192) | |
| ap.add_argument("--kv-cache-dtype", default="fp8") | |
| ap.add_argument("--max-tokens", type=int, default=2048) | |
| ap.add_argument("--reasoning-effort", default="low", choices=["low", "high", "max"]) | |
| ap.add_argument("--seed", type=int, default=1234) | |
| args = ap.parse_args() | |
| rng = random.Random(args.seed) | |
| # Yield here should beat the 48.4% the tool run managed: there is no tool | |
| # execution to corrupt, so a generation fails only on language, format or a | |
| # wrong graded answer. | |
| n_raw = args.n if args.bake_off else int(args.target / 0.70) | |
| print(f"loading seeds from {MERGED.name} ...", flush=True) | |
| jobs = load_seeds(rng, n_raw) | |
| print(f"planned {len(jobs):,} generations " | |
| f"(target {args.target:,} verified)\n", flush=True) | |
| if not jobs: | |
| raise SystemExit("no usable seeds found") | |
| prompts = [ | |
| ENC.encode_messages( | |
| [{"role": "system", "content": SYSTEM}, {"role": "user", "content": q}], | |
| thinking_mode=THINKING, reasoning_effort=args.reasoning_effort) | |
| for q, _, _ in jobs | |
| ] | |
| from vllm import LLM, SamplingParams | |
| llm = LLM(model=args.model, tensor_parallel_size=args.tp, trust_remote_code=True, | |
| max_model_len=args.max_model_len, gpu_memory_utilization=0.90, | |
| kv_cache_dtype=args.kv_cache_dtype) | |
| sp = SamplingParams(temperature=0.7, top_p=0.95, | |
| max_tokens=args.max_tokens, seed=args.seed) | |
| t0 = time.time() | |
| outs = llm.generate(prompts, sp) | |
| dt = time.time() - t0 | |
| n_tok = sum(len(o.outputs[0].token_ids) for o in outs) | |
| print(f"\ngenerated {len(outs):,} in {dt/60:.1f} min | {n_tok:,} tokens | " | |
| f"{n_tok/max(dt,1):,.0f} tok/s", flush=True) | |
| kept, why = [], {} | |
| for i, (o, (q, key, src)) in enumerate(zip(outs, jobs)): | |
| parsed = parse_completion(o.outputs[0]) | |
| if parsed is None: | |
| why["unparseable_or_truncated"] = why.get("unparseable_or_truncated", 0) + 1 | |
| continue | |
| rec, reason = build_record(parsed, q, key, src, i) | |
| if rec: | |
| kept.append(rec) | |
| else: | |
| why[reason] = why.get(reason, 0) + 1 | |
| before = len(kept) | |
| kept = dedup(kept) | |
| print(f"\ngates passed {before:,}/{len(outs):,} ({before/len(outs)*100:.1f}%)") | |
| print(f"after dedup {len(kept):,} (removed {before-len(kept):,})") | |
| print("rejected:") | |
| for k, v in sorted(why.items(), key=lambda x: -x[1]): | |
| print(f" {k:28s} {v:>7,}") | |
| verified = sum(1 for r in kept if r["category"] == "korean_cot_verified") | |
| print(f"\nanswer-verified {verified:,} / {len(kept):,}") | |
| out = REPO / args.out | |
| out.parent.mkdir(parents=True, exist_ok=True) | |
| if args.bake_off: | |
| out = out.with_suffix(".bakeoff.jsonl") | |
| with open(out, "w", encoding="utf-8") as f: | |
| for r in kept: | |
| f.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| print(f"-> {out}") | |
| if args.bake_off and kept: | |
| print("\n===== SAMPLE (review before committing GPU hours) =====") | |
| for r in kept[:2]: | |
| print(f"\n--- seed={r['seed_source']} category={r['category']}") | |
| print(f"Q: {r['messages'][0]['content'][:220]}") | |
| print(f"A: {r['messages'][1]['content'][:700]}") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 11.6 kB
- Xet hash:
- 335aa93ccd0be4d9ca787e9b4d20260596a2f18e733ae10b2d0fb870337d6184
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.