mindXtrain / mindxtrain /data /synth.py
Gregory-L's picture
fork mindXtrain from GitHub (Professor-Codephreak/mindXtrain@661bd41) as the mindX-specific line
dfb775d verified
Raw
History Blame Contribute Delete
3.66 kB
"""Synthetic data generation via teacher models.
Drives bulk-prompt rollouts through an OpenAI-compatible teacher endpoint
(default vLLM-ROCm at `MINDXTRAIN_TEACHER_BASE_URL`). Useful for filling
gaps in the curated corpus (style transfer, persona conditioning, tool-call
trajectories).
"""
from __future__ import annotations
import os
from collections.abc import Iterable, Iterator
from typing import Literal
import httpx
from pydantic import BaseModel, ConfigDict, Field
class SynthRecipe(BaseModel):
model_config = ConfigDict(extra="forbid")
teacher: str = Field(default="Qwen/Qwen3.5-8B", description="HF id of the teacher model")
seeds: list[str] = Field(default_factory=list)
n_per_seed: int = Field(default=8, ge=1)
style: Literal["sft", "dpo", "tool_use"] = "sft"
temperature: float = Field(default=0.9, ge=0.0, le=2.0)
max_tokens: int = Field(default=512, ge=1)
def _post_completion(
base_url: str,
teacher: str,
prompt: str,
*,
temperature: float,
max_tokens: int,
timeout_s: float,
) -> str:
body = {
"model": teacher,
"messages": [{"role": "user", "content": prompt}],
"temperature": temperature,
"max_tokens": max_tokens,
}
with httpx.Client(timeout=timeout_s) as client:
resp = client.post(f"{base_url}/chat/completions", json=body)
resp.raise_for_status()
data = resp.json()
return ((data.get("choices") or [{}])[0].get("message") or {}).get("content", "") or ""
def synthesize(recipe: SynthRecipe, *, base_url: str | None = None, timeout_s: float = 120.0) -> Iterator[dict[str, str]]:
"""Yield synthetic samples per `recipe`. POSTs to a vLLM-compatible endpoint."""
base_url = (
base_url or os.environ.get("MINDXTRAIN_TEACHER_BASE_URL", "http://localhost:8000/v1")
).rstrip("/")
teacher = os.environ.get("MINDXTRAIN_TEACHER_MODEL", recipe.teacher)
for seed in recipe.seeds:
for i in range(recipe.n_per_seed):
response = _post_completion(
base_url,
teacher,
seed,
temperature=recipe.temperature,
max_tokens=recipe.max_tokens,
timeout_s=timeout_s,
)
yield {
"seed": seed,
"rollout_index": str(i),
"response": response,
"style": recipe.style,
}
def merge_synth_with_real(
synth: Iterable[dict[str, str]],
real: Iterable[dict[str, str]],
ratio: float = 0.3,
) -> Iterator[dict[str, str]]:
"""Interleave synth and real samples at the given synth-share ratio.
Deterministic round-robin: every k-th sample is synth where k=1/ratio.
"""
if not (0.0 <= ratio <= 1.0):
msg = f"ratio must be in [0,1]; got {ratio}"
raise ValueError(msg)
if ratio == 0.0:
yield from real
return
if ratio == 1.0:
yield from synth
return
real_iter = iter(real)
synth_iter = iter(synth)
counter = 0.0
for _ in range(10**9): # effectively infinite; consumer breaks
counter += ratio
if counter >= 1.0:
counter -= 1.0
try:
yield next(synth_iter)
except StopIteration:
# synth exhausted; fall through to real-only
yield from real_iter
return
else:
try:
yield next(real_iter)
except StopIteration:
yield from synth_iter
return
__all__ = ["SynthRecipe", "merge_synth_with_real", "synthesize"]