Compare commits
4 Commits
4ec1b84d96
...
1e0981d1e3
| Author | SHA1 | Date |
|---|---|---|
|
|
1e0981d1e3 | |
|
|
cf2329fab0 | |
|
|
8cbbda005c | |
|
|
6a98206aef |
|
|
@ -1,5 +1,7 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
.codex_pytest_tmp*/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
out
|
out
|
||||||
website/
|
website/
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class OpenAICompatibleGenerator:
|
||||||
temperature: float = 0.7,
|
temperature: float = 0.7,
|
||||||
max_tokens: int = 1024,
|
max_tokens: int = 1024,
|
||||||
open_thinking: bool = False,
|
open_thinking: bool = False,
|
||||||
|
seed: int | None = None,
|
||||||
client: Any = None,
|
client: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if client is None:
|
if client is None:
|
||||||
|
|
@ -30,15 +31,23 @@ class OpenAICompatibleGenerator:
|
||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
self.max_tokens = max_tokens
|
self.max_tokens = max_tokens
|
||||||
self.open_thinking = open_thinking
|
self.open_thinking = open_thinking
|
||||||
|
self.seed = seed
|
||||||
|
self._request_index = 0
|
||||||
|
|
||||||
def __call__(self, prompt: str) -> str:
|
def __call__(self, prompt: str) -> str:
|
||||||
|
request = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
|
"stream": False,
|
||||||
|
"extra_body": {"open_thinking": self.open_thinking},
|
||||||
|
}
|
||||||
|
if self.seed is not None:
|
||||||
|
request["seed"] = self.seed + self._request_index
|
||||||
|
self._request_index += 1
|
||||||
response = self.client.chat.completions.create(
|
response = self.client.chat.completions.create(
|
||||||
model=self.model,
|
**request,
|
||||||
messages=[{"role": "user", "content": prompt}],
|
|
||||||
temperature=self.temperature,
|
|
||||||
max_tokens=self.max_tokens,
|
|
||||||
stream=False,
|
|
||||||
extra_body={"open_thinking": self.open_thinking},
|
|
||||||
)
|
)
|
||||||
if not response.choices or response.choices[0].message is None:
|
if not response.choices or response.choices[0].message is None:
|
||||||
raise RuntimeError("Model API returned no completion choice.")
|
raise RuntimeError("Model API returned no completion choice.")
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,35 @@ Each JSONL record contains a prompt, a required function name, and JSON-serializ
|
||||||
|
|
||||||
`dataset/code_rl_mini.jsonl` contains four smoke-test tasks. It is deliberately too small for a meaningful training claim; use it to validate the pipeline before preparing a larger train/validation/test split.
|
`dataset/code_rl_mini.jsonl` contains four smoke-test tasks. It is deliberately too small for a meaningful training claim; use it to validate the pipeline before preparing a larger train/validation/test split.
|
||||||
|
|
||||||
|
For a standard public benchmark, convert the compatible subset of MBPP-sanitized while preserving its official train/validation/test split:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/prepare_mbpp.py --output-dir out/mbpp_sanitized
|
||||||
|
```
|
||||||
|
|
||||||
|
The converter accepts direct, JSON-serializable function assertions, validates every converted task against the published reference solution, and reports all skipped records. It also emits `train_sft.jsonl` from the official training references, enabling an SFT → execution-reward GRPO ablation without leaking validation or test solutions. Generated benchmark files remain under the ignored `out/` directory.
|
||||||
|
|
||||||
## Evaluate saved candidates
|
## Evaluate saved candidates
|
||||||
|
|
||||||
|
Generate multiple candidates from a running OpenAI-compatible model service with deterministic per-request seeds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/generate_codegen.py \
|
||||||
|
--tasks dataset/code_rl_mini.jsonl \
|
||||||
|
--samples-per-task 10 \
|
||||||
|
--seed 42 \
|
||||||
|
--output out/code_predictions.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
Each completed task is flushed to JSONL immediately. If a long run is interrupted, rerun the same command with `--resume`; task-specific seed ranges keep completed and resumed outputs reproducible.
|
||||||
|
|
||||||
|
Then execute and aggregate them:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/eval_codegen.py \
|
python scripts/eval_codegen.py \
|
||||||
--tasks dataset/code_rl_mini.jsonl \
|
--tasks dataset/code_rl_mini.jsonl \
|
||||||
--predictions tests/fixtures/code_rl_predictions.jsonl \
|
--predictions out/code_predictions.jsonl \
|
||||||
--ks 1 2
|
--ks 1 5 10
|
||||||
```
|
```
|
||||||
|
|
||||||
The report separates:
|
The report separates:
|
||||||
|
|
@ -111,7 +133,7 @@ Use the same held-out tasks and sampling parameters for three comparisons:
|
||||||
2. inference-time execution feedback and repair;
|
2. inference-time execution feedback and repair;
|
||||||
3. execution-reward GRPO/CISPO followed by the same inference procedure.
|
3. execution-reward GRPO/CISPO followed by the same inference procedure.
|
||||||
|
|
||||||
Record pass@1, pass@k, test pass rate, compile rate, timeout rate, average attempts, average generation length, reward mean/std, KL to the reference model, and degenerate-group rate. Model results should only be added to the resume after this protocol has been run on AutoDL and the raw logs are retained.
|
Record pass@1, pass@k, test pass rate, compile rate, timeout rate, average attempts, average generation length, reward mean/std, KL to the reference model, and degenerate-group rate. Publish model results only after this protocol has been run and the raw logs and configuration have been retained for reproduction.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
datasets==3.6.0
|
datasets==3.6.0
|
||||||
datasketch==1.6.4
|
datasketch==1.6.4
|
||||||
|
fastapi==0.115.6
|
||||||
Flask==3.0.3
|
Flask==3.0.3
|
||||||
Flask_Cors==4.0.0
|
Flask_Cors==4.0.0
|
||||||
jieba==0.42.1
|
jieba==0.42.1
|
||||||
|
|
@ -23,6 +24,7 @@ jinja2==3.1.2
|
||||||
jsonlines==4.0.0
|
jsonlines==4.0.0
|
||||||
trl==0.13.0
|
trl==0.13.0
|
||||||
ujson==5.1.0
|
ujson==5.1.0
|
||||||
|
uvicorn==0.34.0
|
||||||
wandb==0.18.3
|
wandb==0.18.3
|
||||||
streamlit==1.50.0
|
streamlit==1.50.0
|
||||||
einops==0.8.1
|
einops==0.8.1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
"""Generate reproducible multi-sample candidates for execution-based evaluation."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from code_agent import CodeTask, OpenAICompatibleGenerator
|
||||||
|
|
||||||
|
|
||||||
|
def read_tasks(path):
|
||||||
|
tasks = []
|
||||||
|
with Path(path).open("r", encoding="utf-8") as handle:
|
||||||
|
for line_number, line in enumerate(handle, start=1):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
tasks.append(CodeTask.from_dict(json.loads(line)))
|
||||||
|
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||||
|
raise ValueError(f"Invalid task at {path}:{line_number}: {exc}") from exc
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
def build_prompt(task):
|
||||||
|
return (
|
||||||
|
"You are solving a Python algorithm task. Return exactly one ```python``` code block.\n"
|
||||||
|
f"Required entry point: {task.entry_point}\n"
|
||||||
|
f"Task:\n{task.prompt}\n"
|
||||||
|
"Do not read files, access the network, spawn processes, or call unsafe builtins."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_predictions(tasks, generator, samples_per_task):
|
||||||
|
if samples_per_task < 1:
|
||||||
|
raise ValueError("samples_per_task must be at least 1")
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"task_id": task.task_id,
|
||||||
|
"candidates": [generator(build_prompt(task)) for _ in range(samples_per_task)],
|
||||||
|
}
|
||||||
|
for task in tasks
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def read_completed_task_ids(path):
|
||||||
|
completed = set()
|
||||||
|
if not Path(path).exists():
|
||||||
|
return completed
|
||||||
|
with Path(path).open("r", encoding="utf-8") as handle:
|
||||||
|
for line_number, line in enumerate(handle, start=1):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
record = json.loads(line)
|
||||||
|
completed.add(str(record["task_id"]))
|
||||||
|
except (KeyError, json.JSONDecodeError) as exc:
|
||||||
|
raise ValueError(f"Invalid existing prediction at {path}:{line_number}: {exc}") from exc
|
||||||
|
return completed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate code candidates through an OpenAI-compatible API")
|
||||||
|
parser.add_argument("--tasks", required=True, help="CodeTask JSONL file")
|
||||||
|
parser.add_argument("--output", required=True, help="Prediction JSONL output path")
|
||||||
|
parser.add_argument("--samples-per-task", type=int, default=10)
|
||||||
|
parser.add_argument("--base-url", default="http://localhost:8998/v1")
|
||||||
|
parser.add_argument("--api-key", default="not-needed")
|
||||||
|
parser.add_argument("--model", default="minimind")
|
||||||
|
parser.add_argument("--temperature", type=float, default=0.8)
|
||||||
|
parser.add_argument("--max-tokens", type=int, default=512)
|
||||||
|
parser.add_argument("--seed", type=int, default=42)
|
||||||
|
parser.add_argument("--open-thinking", action="store_true")
|
||||||
|
parser.add_argument("--resume", action="store_true", help="Append missing tasks to an existing output file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
tasks = read_tasks(args.tasks)
|
||||||
|
output_path = Path(args.output)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
completed = read_completed_task_ids(output_path) if args.resume else set()
|
||||||
|
mode = "a" if args.resume else "w"
|
||||||
|
generated_count = 0
|
||||||
|
with output_path.open(mode, encoding="utf-8") as handle:
|
||||||
|
for task_index, task in enumerate(tasks):
|
||||||
|
if task.task_id in completed:
|
||||||
|
print(f"[{task_index + 1}/{len(tasks)}] {task.task_id}: already complete", flush=True)
|
||||||
|
continue
|
||||||
|
generator = OpenAICompatibleGenerator(
|
||||||
|
base_url=args.base_url,
|
||||||
|
api_key=args.api_key,
|
||||||
|
model=args.model,
|
||||||
|
temperature=args.temperature,
|
||||||
|
max_tokens=args.max_tokens,
|
||||||
|
open_thinking=args.open_thinking,
|
||||||
|
seed=args.seed + task_index * args.samples_per_task,
|
||||||
|
)
|
||||||
|
record = generate_predictions([task], generator, args.samples_per_task)[0]
|
||||||
|
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||||
|
handle.flush()
|
||||||
|
generated_count += args.samples_per_task
|
||||||
|
print(f"[{task_index + 1}/{len(tasks)}] {task.task_id}: wrote {args.samples_per_task} candidates", flush=True)
|
||||||
|
print(f"Generated {generated_count} new candidates -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""Convert compatible MBPP-sanitized assertions into MiniMind CodeTask splits."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from code_agent import CodeTask, VerificationStatus, verify_code
|
||||||
|
|
||||||
|
|
||||||
|
def _literal(node):
|
||||||
|
value = ast.literal_eval(node)
|
||||||
|
json.dumps(value, ensure_ascii=False)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_assertion(source):
|
||||||
|
statement = ast.parse(source.strip()).body
|
||||||
|
if len(statement) != 1 or not isinstance(statement[0], ast.Assert):
|
||||||
|
raise ValueError("test is not a single assert statement")
|
||||||
|
expression = statement[0].test
|
||||||
|
|
||||||
|
if isinstance(expression, ast.Call):
|
||||||
|
call, expected = expression, True
|
||||||
|
elif isinstance(expression, ast.UnaryOp) and isinstance(expression.op, ast.Not) and isinstance(expression.operand, ast.Call):
|
||||||
|
call, expected = expression.operand, False
|
||||||
|
elif (
|
||||||
|
isinstance(expression, ast.Compare)
|
||||||
|
and len(expression.ops) == 1
|
||||||
|
and isinstance(expression.ops[0], ast.Eq)
|
||||||
|
and len(expression.comparators) == 1
|
||||||
|
):
|
||||||
|
left, right = expression.left, expression.comparators[0]
|
||||||
|
if isinstance(left, ast.Call):
|
||||||
|
call, expected = left, _literal(right)
|
||||||
|
elif isinstance(right, ast.Call):
|
||||||
|
call, expected = right, _literal(left)
|
||||||
|
else:
|
||||||
|
raise ValueError("equality does not compare a function call")
|
||||||
|
else:
|
||||||
|
raise ValueError("unsupported assertion shape")
|
||||||
|
|
||||||
|
if not isinstance(call.func, ast.Name):
|
||||||
|
raise ValueError("entry point is not a direct function call")
|
||||||
|
args = [_literal(argument) for argument in call.args]
|
||||||
|
kwargs = {}
|
||||||
|
for keyword in call.keywords:
|
||||||
|
if keyword.arg is None:
|
||||||
|
raise ValueError("expanded keyword arguments are unsupported")
|
||||||
|
kwargs[keyword.arg] = _literal(keyword.value)
|
||||||
|
return call.func.id, {"args": args, "kwargs": kwargs, "expected": expected}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_record(record):
|
||||||
|
parsed = [parse_assertion(test) for test in record["test_list"]]
|
||||||
|
entry_points = {entry_point for entry_point, _ in parsed}
|
||||||
|
if len(entry_points) != 1:
|
||||||
|
raise ValueError("tests use multiple entry points")
|
||||||
|
payload = {
|
||||||
|
"task_id": f"mbpp-{record['task_id']}",
|
||||||
|
"prompt": record["prompt"].strip(),
|
||||||
|
"entry_point": entry_points.pop(),
|
||||||
|
"tests": [test for _, test in parsed],
|
||||||
|
}
|
||||||
|
task = CodeTask.from_dict(payload)
|
||||||
|
verification = verify_code(record["code"], task)
|
||||||
|
if verification.status != VerificationStatus.PASSED:
|
||||||
|
raise ValueError(f"reference solution failed verifier: {verification.status.value}")
|
||||||
|
return task.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def build_sft_record(record, task):
|
||||||
|
return {
|
||||||
|
"conversations": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
"Solve the Python algorithm task below. Return one complete ```python``` code block.\n"
|
||||||
|
f"Required entry point: {task['entry_point']}\n"
|
||||||
|
f"Task:\n{task['prompt']}"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{"role": "assistant", "content": f"```python\n{record['code'].strip()}\n```"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_split(records):
|
||||||
|
converted = []
|
||||||
|
sft_records = []
|
||||||
|
skipped = Counter()
|
||||||
|
for record in records:
|
||||||
|
try:
|
||||||
|
task = convert_record(record)
|
||||||
|
converted.append(task)
|
||||||
|
sft_records.append(build_sft_record(record, task))
|
||||||
|
except (KeyError, SyntaxError, TypeError, ValueError) as exc:
|
||||||
|
skipped[str(exc)] += 1
|
||||||
|
return converted, sft_records, skipped
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Prepare verifier-compatible MBPP sanitized splits")
|
||||||
|
parser.add_argument("--dataset", default="google-research-datasets/mbpp")
|
||||||
|
parser.add_argument("--config", default="sanitized")
|
||||||
|
parser.add_argument("--splits", nargs="+", default=["train", "validation", "test"])
|
||||||
|
parser.add_argument("--output-dir", default="out/mbpp_sanitized")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
from datasets import load_dataset
|
||||||
|
|
||||||
|
output_dir = Path(args.output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
summary = {}
|
||||||
|
for split in args.splits:
|
||||||
|
records = load_dataset(args.dataset, args.config, split=split)
|
||||||
|
converted, sft_records, skipped = convert_split(records)
|
||||||
|
output_path = output_dir / f"{split}.jsonl"
|
||||||
|
with output_path.open("w", encoding="utf-8") as handle:
|
||||||
|
for record in converted:
|
||||||
|
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||||
|
sft_output = None
|
||||||
|
if split == "train":
|
||||||
|
sft_output = output_dir / "train_sft.jsonl"
|
||||||
|
with sft_output.open("w", encoding="utf-8") as handle:
|
||||||
|
for record in sft_records:
|
||||||
|
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||||
|
summary[split] = {
|
||||||
|
"source_count": len(records),
|
||||||
|
"converted_count": len(converted),
|
||||||
|
"skipped_count": len(records) - len(converted),
|
||||||
|
"top_skip_reasons": skipped.most_common(10),
|
||||||
|
"output": str(output_path),
|
||||||
|
"sft_output": str(sft_output) if sft_output else None,
|
||||||
|
}
|
||||||
|
print(f"{split}: converted {len(converted)}/{len(records)} -> {output_path}")
|
||||||
|
(output_dir / "summary.json").write_text(
|
||||||
|
json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -56,6 +56,7 @@ class ChatRequest(BaseModel):
|
||||||
stream: bool = True
|
stream: bool = True
|
||||||
tools: list = Field(default_factory=list)
|
tools: list = Field(default_factory=list)
|
||||||
open_thinking: bool = False
|
open_thinking: bool = False
|
||||||
|
seed: int | None = None
|
||||||
chat_template_kwargs: dict = None
|
chat_template_kwargs: dict = None
|
||||||
|
|
||||||
def get_open_thinking(self) -> bool:
|
def get_open_thinking(self) -> bool:
|
||||||
|
|
@ -102,7 +103,7 @@ def parse_response(text):
|
||||||
return text.strip(), reasoning_content, tool_calls or None
|
return text.strip(), reasoning_content, tool_calls or None
|
||||||
|
|
||||||
|
|
||||||
def generate_stream_response(messages, temperature, top_p, max_tokens, tools=None, open_thinking=False):
|
def generate_stream_response(messages, temperature, top_p, max_tokens, tools=None, open_thinking=False, seed=None):
|
||||||
try:
|
try:
|
||||||
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)
|
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)
|
||||||
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
||||||
|
|
@ -112,6 +113,7 @@ def generate_stream_response(messages, temperature, top_p, max_tokens, tools=Non
|
||||||
|
|
||||||
def _generate():
|
def _generate():
|
||||||
try:
|
try:
|
||||||
|
generator = torch.Generator(device=inputs.input_ids.device).manual_seed(seed) if seed is not None else None
|
||||||
model.generate(
|
model.generate(
|
||||||
inputs.input_ids,
|
inputs.input_ids,
|
||||||
max_new_tokens=max_tokens,
|
max_new_tokens=max_tokens,
|
||||||
|
|
@ -121,7 +123,8 @@ def generate_stream_response(messages, temperature, top_p, max_tokens, tools=Non
|
||||||
attention_mask=inputs.attention_mask,
|
attention_mask=inputs.attention_mask,
|
||||||
pad_token_id=tokenizer.pad_token_id,
|
pad_token_id=tokenizer.pad_token_id,
|
||||||
eos_token_id=tokenizer.eos_token_id,
|
eos_token_id=tokenizer.eos_token_id,
|
||||||
streamer=streamer
|
streamer=streamer,
|
||||||
|
generator=generator
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
queue.put({"error": str(e)})
|
queue.put({"error": str(e)})
|
||||||
|
|
@ -186,7 +189,8 @@ async def chat_completions(request: ChatRequest):
|
||||||
top_p=request.top_p,
|
top_p=request.top_p,
|
||||||
max_tokens=request.max_tokens,
|
max_tokens=request.max_tokens,
|
||||||
tools=request.tools,
|
tools=request.tools,
|
||||||
open_thinking=request.get_open_thinking()
|
open_thinking=request.get_open_thinking(),
|
||||||
|
seed=request.seed
|
||||||
)),
|
)),
|
||||||
media_type="text/event-stream"
|
media_type="text/event-stream"
|
||||||
)
|
)
|
||||||
|
|
@ -200,6 +204,7 @@ async def chat_completions(request: ChatRequest):
|
||||||
)
|
)
|
||||||
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
|
generator = torch.Generator(device=inputs["input_ids"].device).manual_seed(request.seed) if request.seed is not None else None
|
||||||
generated_ids = model.generate(
|
generated_ids = model.generate(
|
||||||
inputs["input_ids"],
|
inputs["input_ids"],
|
||||||
max_length=inputs["input_ids"].shape[1] + request.max_tokens,
|
max_length=inputs["input_ids"].shape[1] + request.max_tokens,
|
||||||
|
|
@ -208,7 +213,8 @@ async def chat_completions(request: ChatRequest):
|
||||||
pad_token_id=tokenizer.pad_token_id,
|
pad_token_id=tokenizer.pad_token_id,
|
||||||
eos_token_id=tokenizer.eos_token_id,
|
eos_token_id=tokenizer.eos_token_id,
|
||||||
top_p=request.top_p,
|
top_p=request.top_p,
|
||||||
temperature=request.temperature
|
temperature=request.temperature,
|
||||||
|
generator=generator
|
||||||
)
|
)
|
||||||
answer = tokenizer.decode(generated_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
|
answer = tokenizer.decode(generated_ids[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
|
||||||
content, reasoning_content, tool_calls = parse_response(answer)
|
content, reasoning_content, tool_calls = parse_response(answer)
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,17 @@ def test_openai_backend_builds_non_streaming_request():
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
max_tokens=128,
|
max_tokens=128,
|
||||||
open_thinking=True,
|
open_thinking=True,
|
||||||
|
seed=17,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert generator("solve it").startswith("```python")
|
assert generator("solve it").startswith("```python")
|
||||||
assert completions.kwargs["messages"][0]["content"] == "solve it"
|
assert completions.kwargs["messages"][0]["content"] == "solve it"
|
||||||
assert completions.kwargs["stream"] is False
|
assert completions.kwargs["stream"] is False
|
||||||
assert completions.kwargs["extra_body"] == {"open_thinking": True}
|
assert completions.kwargs["extra_body"] == {"open_thinking": True}
|
||||||
|
assert completions.kwargs["seed"] == 17
|
||||||
|
|
||||||
|
generator("solve another")
|
||||||
|
assert completions.kwargs["seed"] == 18
|
||||||
|
|
||||||
|
|
||||||
def test_openai_backend_rejects_empty_completion():
|
def test_openai_backend_rejects_empty_completion():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from scripts.generate_codegen import build_prompt, generate_predictions, read_completed_task_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_predictions_groups_samples_by_task():
|
||||||
|
task = SimpleNamespace(task_id="add", entry_point="add", prompt="Implement add(a, b).")
|
||||||
|
prompts = []
|
||||||
|
|
||||||
|
def generate(prompt):
|
||||||
|
prompts.append(prompt)
|
||||||
|
return f"candidate-{len(prompts)}"
|
||||||
|
|
||||||
|
records = generate_predictions([task], generate, samples_per_task=3)
|
||||||
|
|
||||||
|
assert records == [{"task_id": "add", "candidates": ["candidate-1", "candidate-2", "candidate-3"]}]
|
||||||
|
assert all("Required entry point: add" in prompt for prompt in prompts)
|
||||||
|
assert "exactly one ```python``` code block" in build_prompt(task)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_predictions_rejects_empty_sample_count():
|
||||||
|
with pytest.raises(ValueError, match="at least 1"):
|
||||||
|
generate_predictions([], lambda prompt: prompt, samples_per_task=0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_completed_task_ids_supports_resume(tmp_path):
|
||||||
|
output = tmp_path / "predictions.jsonl"
|
||||||
|
output.write_text(
|
||||||
|
'{"task_id":"one","candidates":["a"]}\n'
|
||||||
|
'{"task_id":"two","candidates":["b"]}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert read_completed_task_ids(output) == {"one", "two"}
|
||||||
|
assert read_completed_task_ids(tmp_path / "missing.jsonl") == set()
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from scripts.prepare_mbpp import build_sft_record, convert_record, convert_split, parse_assertion
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_mbpp_equality_and_boolean_assertions():
|
||||||
|
assert parse_assertion("assert add(1, 2) == 3") == (
|
||||||
|
"add",
|
||||||
|
{"args": [1, 2], "kwargs": {}, "expected": 3},
|
||||||
|
)
|
||||||
|
assert parse_assertion("assert is_even(4)")[-1]["expected"] is True
|
||||||
|
assert parse_assertion("assert not is_even(3)")[-1]["expected"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_record_validates_reference_solution():
|
||||||
|
record = {
|
||||||
|
"task_id": 1,
|
||||||
|
"prompt": "Write a function that adds two integers.",
|
||||||
|
"code": "def add(a, b):\n return a + b",
|
||||||
|
"test_list": ["assert add(1, 2) == 3", "assert add(-1, 1) == 0"],
|
||||||
|
}
|
||||||
|
|
||||||
|
converted = convert_record(record)
|
||||||
|
|
||||||
|
assert converted["task_id"] == "mbpp-1"
|
||||||
|
assert converted["entry_point"] == "add"
|
||||||
|
assert len(converted["tests"]) == 2
|
||||||
|
|
||||||
|
sft = build_sft_record(record, converted)
|
||||||
|
assert "Required entry point: add" in sft["conversations"][0]["content"]
|
||||||
|
assert "def add(a, b)" in sft["conversations"][1]["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_split_keeps_tasks_and_sft_records_aligned():
|
||||||
|
record = {
|
||||||
|
"task_id": 2,
|
||||||
|
"prompt": "Return the input integer.",
|
||||||
|
"code": "def identity(value):\n return value",
|
||||||
|
"test_list": ["assert identity(7) == 7"],
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, sft_records, skipped = convert_split([record])
|
||||||
|
|
||||||
|
assert len(tasks) == len(sft_records) == 1
|
||||||
|
assert not skipped
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_mbpp_rejects_nonliteral_expectation():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
parse_assertion("assert add(1, 2) == expected")
|
||||||
|
|
@ -168,7 +168,7 @@ def grpo_train_epoch(epoch, loader, iters, rollout_engine, ref_model, reward_mod
|
||||||
current_aux_loss = aux_loss.item()
|
current_aux_loss = aux_loss.item()
|
||||||
avg_reward_val = rewards.mean().item()
|
avg_reward_val = rewards.mean().item()
|
||||||
avg_len_val = completion_mask.sum(dim=1).float().mean().item()
|
avg_len_val = completion_mask.sum(dim=1).float().mean().item()
|
||||||
kl_ref_val = ((ref_per_token_logps - per_token_logps) * completion_mask).sum().item() / max(completion_mask.sum().item(), 1)
|
kl_ref_val = (per_token_kl * completion_mask).sum().item() / max(completion_mask.sum().item(), 1)
|
||||||
advantages_mean_val = advantages.mean().item()
|
advantages_mean_val = advantages.mean().item()
|
||||||
advantages_std_val = advantages.std().item()
|
advantages_std_val = advantages.std().item()
|
||||||
current_lr = optimizer.param_groups[0]['lr']
|
current_lr = optimizer.param_groups[0]['lr']
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue