fix(data-process): 发布精确三路数据切分

This commit is contained in:
caoxiaozhu
2026-07-24 20:43:47 +08:00
parent e6a5a36bc0
commit 9cb77c251a
8 changed files with 405 additions and 181 deletions

View File

@@ -534,6 +534,12 @@ async def _sync_training_dataset_to_compute_node(
files = store.training_dataset_files(dataset_id) files = store.training_dataset_files(dataset_id)
if not files: if not files:
raise RuntimeError(f"dataset has no uploaded file: {dataset_id}") raise RuntimeError(f"dataset has no uploaded file: {dataset_id}")
split_aware = any(item.get("split") for item in files)
files = [
item
for item in files
if not split_aware or item.get("split") in {"train", "validation"}
]
client = ComputeNodeClient(node["api_base_url"]) client = ComputeNodeClient(node["api_base_url"])
results: list[dict[str, Any]] = [] results: list[dict[str, Any]] = []
for item in files: for item in files:
@@ -1533,4 +1539,3 @@ async def log_content(file: str = Query(...)) -> dict[str, Any]:
@router.post("/web-log") @router.post("/web-log")
async def web_log(payload: dict[str, Any] = Body(...)) -> dict[str, Any]: async def web_log(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
return ok({"received": True, **payload}) return ok({"received": True, **payload})

View File

@@ -1099,11 +1099,28 @@ class PlatformStore:
def _dataset(self, conn: PgConnection, row: PgRow) -> dict[str, Any]: def _dataset(self, conn: PgConnection, row: PgRow) -> dict[str, Any]:
files = conn.execute( files = conn.execute(
"SELECT id, name, size, active_version_id, create_time FROM dataset_files WHERE dataset_id=? ORDER BY create_time", """SELECT id, name, size, active_version_id, create_time,
record_count, metadata
FROM dataset_files WHERE dataset_id=? ORDER BY create_time, id""",
(row["id"],), (row["id"],),
).fetchall() ).fetchall()
dataset_metadata = json_loads(row.get("metadata"), {})
split_counts = dict(dataset_metadata.get("split_counts") or {})
if row.get("source") == "task" and not split_counts:
split_rows = conn.execute(
"""SELECT split, COUNT(*) AS count FROM dataset_records
WHERE dataset_id=? GROUP BY split""",
(row["id"],),
).fetchall()
split_counts = {str(item["split"]): int(item["count"]) for item in split_rows}
return { return {
**dict(row), **dict(row),
"metadata": dataset_metadata,
"split_counts": {
"train": int(split_counts.get("train", 0) or 0),
"validation": int(split_counts.get("validation", 0) or 0),
"test": int(split_counts.get("test", 0) or 0),
},
"files": [ "files": [
{ {
"id": f["id"], "id": f["id"],
@@ -1111,6 +1128,8 @@ class PlatformStore:
"size": f["size"], "size": f["size"],
"active_version_id": f["active_version_id"], "active_version_id": f["active_version_id"],
"create_time": f["create_time"], "create_time": f["create_time"],
"record_count": int(f.get("record_count") or 0),
"split": json_loads(f.get("metadata"), {}).get("file_split"),
} }
for f in files for f in files
], ],
@@ -1212,14 +1231,22 @@ class PlatformStore:
raise KeyError(dataset_id) raise KeyError(dataset_id)
rows = conn.execute( rows = conn.execute(
""" """
SELECT id, dataset_id, name, size, content, active_version_id, create_time SELECT id, dataset_id, name, size, content, active_version_id,
create_time, record_count, metadata
FROM dataset_files FROM dataset_files
WHERE dataset_id=? WHERE dataset_id=?
ORDER BY create_time ORDER BY create_time
""", """,
(dataset_id,), (dataset_id,),
).fetchall() ).fetchall()
return [dict(row) for row in rows] return [
{
**dict(row),
"metadata": json_loads(row.get("metadata"), {}),
"split": json_loads(row.get("metadata"), {}).get("file_split"),
}
for row in rows
]
def file_versions(self, file_id: str) -> dict[str, Any]: def file_versions(self, file_id: str) -> dict[str, Any]:
row = self.dataset_file(file_id) row = self.dataset_file(file_id)
@@ -1504,15 +1531,36 @@ class PlatformStore:
model = conn.execute("SELECT * FROM models WHERE id=?", (base_model_id,)).fetchone() model = conn.execute("SELECT * FROM models WHERE id=?", (base_model_id,)).fetchone()
dataset = conn.execute("SELECT * FROM datasets WHERE id=?", (dataset_id,)).fetchone() dataset = conn.execute("SELECT * FROM datasets WHERE id=?", (dataset_id,)).fetchone()
files = conn.execute( files = conn.execute(
"SELECT id, name, size, active_version_id, create_time FROM dataset_files WHERE dataset_id=? ORDER BY create_time", """SELECT id, name, size, active_version_id, create_time, metadata
FROM dataset_files WHERE dataset_id=? ORDER BY create_time, id""",
(dataset_id,), (dataset_id,),
).fetchall() ).fetchall()
model_path = (model and model.get("path")) or task.get("model_name_or_path") or base_model_id model_path = (model and model.get("path")) or task.get("model_name_or_path") or base_model_id
if not files: if not files:
raise RuntimeError(f"dataset has no uploaded file: {dataset_id}") raise RuntimeError(f"dataset has no uploaded file: {dataset_id}")
dataset_key = str(task.get("dataset_key") or llama_dataset_key(dataset_id)) dataset_key = str(task.get("dataset_key") or llama_dataset_key(dataset_id))
dataset_file_names = [Path(str(row["name"] or row["id"])).name for row in files] file_entries = [
dataset_keys = llama_dataset_keys(dataset_key, dataset_file_names) {
**dict(row),
"name": Path(str(row["name"] or row["id"])).name,
"split": json_loads(row.get("metadata"), {}).get("file_split"),
}
for row in files
]
split_aware = any(item["split"] for item in file_entries)
training_files = [
item for item in file_entries if not split_aware or item["split"] == "train"
]
validation_files = [
item for item in file_entries if split_aware and item["split"] == "validation"
]
if not training_files:
raise RuntimeError(f"dataset has no training split: {dataset_id}")
runtime_files = [*training_files, *validation_files]
runtime_file_names = [str(item["name"]) for item in runtime_files]
runtime_keys = llama_dataset_keys(dataset_key, runtime_file_names)
training_keys = runtime_keys[: len(training_files)]
validation_keys = runtime_keys[len(training_files) :]
dataset_format = str(task.get("dataset_format") or (dataset and dataset.get("formatting")) or "alpaca").lower() dataset_format = str(task.get("dataset_format") or (dataset and dataset.get("formatting")) or "alpaca").lower()
health_detail = node.get("health_detail") or {} health_detail = node.get("health_detail") or {}
dataset_root = str(health_detail.get("dataset_root") or f"{node['data_root'].rstrip('/')}/datasets") dataset_root = str(health_detail.get("dataset_root") or f"{node['data_root'].rstrip('/')}/datasets")
@@ -1526,23 +1574,26 @@ class PlatformStore:
"name": task["name"], "name": task["name"],
"base_model": model_path, "base_model": model_path,
"model_name_or_path": model_path, "model_name_or_path": model_path,
"dataset": ",".join(dataset_keys), "dataset": ",".join(training_keys),
"dataset_key": dataset_key, "dataset_key": dataset_key,
"dataset_keys": dataset_keys, "dataset_keys": training_keys,
"eval_dataset": ",".join(validation_keys) or None,
"eval_dataset_keys": validation_keys,
"dataset_display_name": (dataset and dataset.get("name")) or dataset_id, "dataset_display_name": (dataset and dataset.get("name")) or dataset_id,
"dataset_dir": dataset_dir, "dataset_dir": dataset_dir,
"dataset_info": llama_dataset_info(dataset_key, dataset_file_names, dataset_format), "dataset_info": llama_dataset_info(dataset_key, runtime_file_names, dataset_format),
"dataset_files": [ "dataset_files": [
{ {
"id": row["id"], "id": item["id"],
"name": Path(str(row["name"] or row["id"])).name, "name": item["name"],
"relative_path": f"{dataset_id}/{Path(str(row['name'] or row['id'])).name}", "relative_path": f"{dataset_id}/{item['name']}",
"local_path": f"{dataset_dir.rstrip('/')}/{Path(str(row['name'] or row['id'])).name}", "local_path": f"{dataset_dir.rstrip('/')}/{item['name']}",
"active_version_id": row["active_version_id"], "active_version_id": item["active_version_id"],
"size": row["size"], "size": item["size"],
"create_time": row["create_time"], "create_time": item["create_time"],
"split": item["split"],
} }
for row in files for item in runtime_files
], ],
"output_dir": output_dir, "output_dir": output_dir,
"gpus": selected_gpus or task.get("gpus") or [0], "gpus": selected_gpus or task.get("gpus") or [0],
@@ -2536,4 +2587,3 @@ def get_platform_store() -> PlatformStore:
if _store is None: if _store is None:
_store = PlatformStore() _store = PlatformStore()
return _store return _store

View File

@@ -2871,6 +2871,51 @@ def stable_split(
return "test" return "test"
def stable_split_assignments(
values: Sequence[str | int],
split: Mapping[str, int] | None = None,
*,
seed: str = "",
) -> list[DatasetSplit]:
"""按稳定顺序和精确配额批量划分数据集。
单条哈希分桶只能在大样本下近似比例。这里先按哈希稳定排序,再用
最大余数法计算各切分配额,确保小数据集也严格遵循配置比例。
"""
ratios = dict(split or {"train": 80, "validation": 10, "test": 10})
# 复用单条划分的参数校验,避免两套规则逐渐漂移。
stable_split("validation", ratios, seed=seed)
if not values:
return []
split_order: tuple[DatasetSplit, ...] = ("train", "validation", "test")
exact = {name: len(values) * ratios[name] / 100 for name in split_order}
quotas = {name: math.floor(exact[name]) for name in split_order}
remaining = len(values) - sum(quotas.values())
remainder_order = sorted(
split_order,
key=lambda name: (-(exact[name] - quotas[name]), split_order.index(name)),
)
for name in remainder_order[:remaining]:
quotas[name] += 1
ranked_indices = sorted(
range(len(values)),
key=lambda index: (
hashlib.sha256(f"{seed}:{values[index]}".encode("utf-8")).digest(),
index,
),
)
assignments: list[DatasetSplit] = ["train"] * len(values)
cursor = 0
for name in split_order:
for index in ranked_indices[cursor : cursor + quotas[name]]:
assignments[index] = name
cursor += quotas[name]
return assignments
def _preview_content(item: Mapping[str, Any]) -> str: def _preview_content(item: Mapping[str, Any]) -> str:
for field in ("edited_content", "editedContent", "original_content", "originalContent", "content"): for field in ("edited_content", "editedContent", "original_content", "originalContent", "content"):
value = item.get(field) value = item.get(field)
@@ -2970,9 +3015,16 @@ def generate_standard_records(
"original_input": input_text, "original_input": input_text,
"original_output": output, "original_output": output,
"status": status, "status": status,
"split": stable_split(result_id, split, seed=split_seed), "split": "train",
} }
) )
assignments = stable_split_assignments(
[str(result["id"]) for result in results],
split,
seed=split_seed,
)
for result, assignment in zip(results, assignments, strict=True):
result["split"] = assignment
return results return results
@@ -3021,4 +3073,5 @@ __all__ = [
"remove_document_noise", "remove_document_noise",
"score_quality", "score_quality",
"stable_split", "stable_split",
"stable_split_assignments",
] ]

View File

@@ -11,7 +11,7 @@ from urllib.parse import urlsplit, urlunsplit
import httpx import httpx
from app.modules.data_process.algorithms import normalize_text, stable_split from app.modules.data_process.algorithms import normalize_text, stable_split_assignments
class ModelGenerationError(ValueError): class ModelGenerationError(ValueError):
@@ -203,7 +203,7 @@ def generate_model_records(
"original_output": "", "original_output": "",
"status": "invalid", "status": "invalid",
"error": error_message, "error": error_message,
"split": stable_split(result_id, split, seed=task_id), "split": "train",
} }
) )
if on_progress: if on_progress:
@@ -236,7 +236,7 @@ def generate_model_records(
"original_output": output, "original_output": output,
"status": "valid" if valid else "invalid", "status": "valid" if valid else "invalid",
"error": None if valid else "model result is missing instruction or output", "error": None if valid else "model result is missing instruction or output",
"split": stable_split(result_id, split, seed=task_id), "split": "train",
} }
) )
if on_progress: if on_progress:
@@ -244,6 +244,13 @@ def generate_model_records(
finally: finally:
if owns_client: if owns_client:
http_client.close() http_client.close()
assignments = stable_split_assignments(
[str(result["id"]) for result in results],
split,
seed=task_id,
)
for result, assignment in zip(results, assignments, strict=True):
result["split"] = assignment
return results return results

View File

@@ -15,7 +15,7 @@ import psycopg
from psycopg.rows import dict_row from psycopg.rows import dict_row
from app.core.config import get_settings from app.core.config import get_settings
from app.modules.data_process.algorithms import estimate_token_count, stable_split from app.modules.data_process.algorithms import estimate_token_count, stable_split_assignments
TASK_STATUSES = {"pending", "running", "completed", "failed", "stopped"} TASK_STATUSES = {"pending", "running", "completed", "failed", "stopped"}
EDITABLE_STATUSES = {"pending", "failed", "stopped", "completed"} EDITABLE_STATUSES = {"pending", "failed", "stopped", "completed"}
@@ -1190,15 +1190,15 @@ class DataProcessStore:
return _decode_row(row) or {} return _decode_row(row) or {}
def publish(self, task_id: str, payload: dict[str, Any]) -> dict[str, Any]: def publish(self, task_id: str, payload: dict[str, Any]) -> dict[str, Any]:
"""发布有效结果;任务行锁保证重复请求返回同一数据集""" """按精确配额发布三个切分文件;重复发布会同步修复既有发布物"""
with self.connect() as conn: with self.connect() as conn:
task = self._task_in_connection(conn, task_id, for_update=True) task = self._task_in_connection(conn, task_id, for_update=True)
existing_dataset = None
if task.get("output_dataset_id"): if task.get("output_dataset_id"):
dataset = conn.execute( existing_dataset = conn.execute(
"SELECT * FROM datasets WHERE id=%s", (task["output_dataset_id"],) "SELECT * FROM datasets WHERE id=%s", (task["output_dataset_id"],)
).fetchone() ).fetchone()
if dataset: if not existing_dataset:
return {"dataset": _decode_row(dataset), "created": False}
# 数据集被外部流程清理后,解除断链并重新发布。 # 数据集被外部流程清理后,解除断链并重新发布。
conn.execute( conn.execute(
"UPDATE data_process_tasks SET output_dataset_id=NULL WHERE id=%s", "UPDATE data_process_tasks SET output_dataset_id=NULL WHERE id=%s",
@@ -1225,44 +1225,106 @@ class DataProcessStore:
if invalid_count: if invalid_count:
raise InvalidStateError(f"task contains {invalid_count} invalid results") raise InvalidStateError(f"task contains {invalid_count} invalid results")
dataset_id = new_id("dataset") dataset_id = (
file_id = new_id("dfile") str(existing_dataset["id"]) if existing_dataset else new_id("dataset")
version_id = new_id("dfv") )
now = utcnow() now = utcnow()
requested_split = payload.get("split") or { requested_split = payload.get("split") or {
"train": 80, "train": 80,
"validation": 10, "validation": 10,
"test": 10, "test": 10,
} }
assignments = stable_split_assignments(
[str(row["id"]) for row in rows],
requested_split,
seed=task_id,
)
records = [ records = [
{ {
"instruction": row["instruction"], "instruction": row["instruction"],
"input": row["input"], "input": row["input"],
"output": row["output"], "output": row["output"],
"split": stable_split( "split": assignment,
str(row["id"]), }
requested_split, for row, assignment in zip(rows, assignments, strict=True)
seed=task_id, ]
split_order = ("train", "validation", "test")
split_counts = {
split_name: assignments.count(split_name) for split_name in split_order
}
file_specs: list[dict[str, Any]] = []
for split_name in split_order:
split_records = [
(source_row, record)
for source_row, record in zip(rows, records, strict=True)
if record["split"] == split_name
]
if not split_records:
continue
file_id = new_id("dfile")
version_id = new_id("dfv")
content = "".join(
json_dumps(record) + "\n" for _, record in split_records
)
raw = content.encode("utf-8")
file_specs.append(
{
"split": split_name,
"records": split_records,
"file_id": file_id,
"version_id": version_id,
"content": content,
"raw": raw,
"checksum": hashlib.sha256(raw).hexdigest(),
"storage_object_id": (
f"db://data-process/{task_id}/{file_id}/v1"
), ),
} }
for row in rows )
] total_size = sum(len(spec["raw"]) for spec in file_specs)
content = "".join(json_dumps(record) + "\n" for record in records)
raw = content.encode("utf-8")
checksum = hashlib.sha256(raw).hexdigest()
storage_object_id = f"db://data-process/{task_id}/{file_id}/v1"
source_result_ids = [row["id"] for row in rows] source_result_ids = [row["id"] for row in rows]
metadata = { metadata = {
"source": "data_process", "source": "data_process",
"storage_backend": "database", "storage_backend": "database",
"storage_object_id": storage_object_id,
"source_task_id": task_id, "source_task_id": task_id,
"source_file_ids": [item["id"] for item in self._source_ids(conn, task_id)], "source_file_ids": [item["id"] for item in self._source_ids(conn, task_id)],
"source_result_ids": source_result_ids, "source_result_ids": source_result_ids,
"format": payload.get("format") or "alpaca_jsonl", "format": payload.get("format") or "alpaca_jsonl",
"split": payload.get("split") or {}, "split": requested_split,
"split_counts": split_counts,
} }
try: try:
if existing_dataset:
# 用户显式重新发布时,在同一数据集 ID 下修复旧的混合文件。
conn.execute(
"DELETE FROM dataset_records WHERE dataset_id=%s", (dataset_id,)
)
conn.execute(
"""DELETE FROM dataset_file_versions
WHERE dataset_file_id IN
(SELECT id FROM dataset_files WHERE dataset_id=%s)""",
(dataset_id,),
)
conn.execute("DELETE FROM dataset_files WHERE dataset_id=%s", (dataset_id,))
dataset = conn.execute(
"""
UPDATE datasets
SET size=%s, size_bytes=%s, count=%s, record_count=%s,
description=%s, metadata=%s, updated_at=%s
WHERE id=%s RETURNING *
""",
(
f"{total_size} B",
total_size,
len(records),
len(records),
payload.get("description") or task.get("description") or "",
json_dumps(metadata),
now,
dataset_id,
),
).fetchone()
else:
dataset = conn.execute( dataset = conn.execute(
""" """
INSERT INTO datasets INSERT INTO datasets
@@ -1281,8 +1343,8 @@ class DataProcessStore:
payload.get("storage_type") or "local", payload.get("storage_type") or "local",
task_id, task_id,
task_id, task_id,
f"{len(raw)} B", f"{total_size} B",
len(raw), total_size,
len(records), len(records),
len(records), len(records),
payload.get("description") or task.get("description") or "", payload.get("description") or task.get("description") or "",
@@ -1296,16 +1358,21 @@ class DataProcessStore:
now, now,
), ),
).fetchone() ).fetchone()
for spec in file_specs:
file_metadata = {**metadata, "file_split": spec["split"]}
version = { version = {
"id": version_id, "id": spec["version_id"],
"version_no": 1, "version_no": 1,
"description": "data process publish", "version": 1,
"checksum_sha256": checksum, "description": f"data process {spec['split']} publish",
"size_bytes": len(raw), "checksum_sha256": spec["checksum"],
"record_count": len(records), "size_bytes": len(spec["raw"]),
"record_count": len(spec["records"]),
"created_at": now, "created_at": now,
"create_time": now,
"source_task_id": task_id, "source_task_id": task_id,
"storage_object_id": storage_object_id, "storage_object_id": spec["storage_object_id"],
} }
conn.execute( conn.execute(
""" """
@@ -1319,25 +1386,25 @@ class DataProcessStore:
%s, %s, 1, %s, %s, %s, %s, %s, %s, %s) %s, %s, 1, %s, %s, %s, %s, %s, %s, %s)
""", """,
( (
file_id, spec["file_id"],
dataset_id, dataset_id,
f"{payload['dataset_name']}.jsonl", f"{payload['dataset_name']}.{spec['split']}.jsonl",
storage_object_id, spec["storage_object_id"],
f"{len(raw)} B", f"{len(spec['raw'])} B",
content, spec["content"],
version_id, spec["version_id"],
json_dumps([version]), json_dumps([version]),
now, now,
version_id, spec["version_id"],
len(raw), len(spec["raw"]),
len(records), len(spec["records"]),
"jsonl", "jsonl",
checksum, spec["checksum"],
task_id, task_id,
task.get("tenant_id"), task.get("tenant_id"),
task.get("project_id"), task.get("project_id"),
payload.get("created_by") or task.get("created_by"), payload.get("created_by") or task.get("created_by"),
json_dumps(metadata), json_dumps(file_metadata),
now, now,
now, now,
), ),
@@ -1351,22 +1418,22 @@ class DataProcessStore:
VALUES (%s, %s, 1, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, 1, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", """,
( (
version_id, spec["version_id"],
file_id, spec["file_id"],
storage_object_id, spec["storage_object_id"],
content[:2000], spec["content"][:2000],
"data process publish", f"data process {spec['split']} publish",
len(raw), len(spec["raw"]),
len(records), len(spec["records"]),
checksum, spec["checksum"],
task_id, task_id,
json_dumps(metadata), json_dumps(file_metadata),
payload.get("created_by") or task.get("created_by"), payload.get("created_by") or task.get("created_by"),
now, now,
), ),
) )
for line_number, (source_row, record) in enumerate( for line_number, (source_row, record) in enumerate(
zip(rows, records, strict=True), start=1 spec["records"], start=1
): ):
conn.execute( conn.execute(
""" """
@@ -1380,8 +1447,8 @@ class DataProcessStore:
( (
new_id("drec"), new_id("drec"),
dataset_id, dataset_id,
file_id, spec["file_id"],
version_id, spec["version_id"],
line_number, line_number,
record["split"], record["split"],
record["instruction"], record["instruction"],
@@ -1412,7 +1479,11 @@ class DataProcessStore:
""", """,
(dataset_id, now, payload.get("created_by"), task_id), (dataset_id, now, payload.get("created_by"), task_id),
) )
return {"dataset": _decode_row(dataset), "created": True} return {
"dataset": _decode_row(dataset),
"created": existing_dataset is None,
"split_counts": split_counts,
}
@staticmethod @staticmethod
def _source_ids( def _source_ids(

View File

@@ -35,6 +35,7 @@ from app.modules.data_process.algorithms import (
remove_document_noise, remove_document_noise,
score_quality, score_quality,
stable_split, stable_split,
stable_split_assignments,
) )
@@ -859,6 +860,17 @@ def test_stable_split_is_reproducible_and_validates_ratios() -> None:
stable_split("record", {"train": 80, "validation": 10, "test": 9}) stable_split("record", {"train": 80, "validation": 10, "test": 9})
def test_stable_split_assignments_use_exact_deterministic_quotas() -> None:
values = [f"record-{index}" for index in range(28)]
first = stable_split_assignments(values, seed="task-1")
second = stable_split_assignments(values, seed="task-1")
assert first == second
assert first.count("train") == 22
assert first.count("validation") == 3
assert first.count("test") == 3
def test_generate_standard_records_supports_json_qa_and_stable_variants() -> None: def test_generate_standard_records_supports_json_qa_and_stable_variants() -> None:
previews = [ previews = [
{ {

View File

@@ -265,6 +265,9 @@ def build_command(config: dict[str, Any], llama_factory_home: str = "/app/LLaMA-
] ]
if dataset_dir: if dataset_dir:
command.extend(["--dataset_dir", str(dataset_dir)]) command.extend(["--dataset_dir", str(dataset_dir)])
eval_dataset = config.get("eval_dataset")
if eval_dataset:
command.extend(["--eval_dataset", str(eval_dataset), "--do_eval", "true"])
_optional_arg(config, command, "--cutoff_len", "max_length", "cutoff_len") _optional_arg(config, command, "--cutoff_len", "max_length", "cutoff_len")
_optional_arg(config, command, "--lr_scheduler_type", "lr_scheduler_type") _optional_arg(config, command, "--lr_scheduler_type", "lr_scheduler_type")
_optional_arg(config, command, "--warmup_ratio", "warmup_ratio") _optional_arg(config, command, "--warmup_ratio", "warmup_ratio")
@@ -273,6 +276,7 @@ def build_command(config: dict[str, Any], llama_factory_home: str = "/app/LLaMA-
_optional_arg(config, command, "--lora_alpha", "lora_alpha") _optional_arg(config, command, "--lora_alpha", "lora_alpha")
_optional_arg(config, command, "--lora_dropout", "lora_dropout") _optional_arg(config, command, "--lora_dropout", "lora_dropout")
_optional_arg(config, command, "--gradient_accumulation_steps", "gradient_accumulation_steps") _optional_arg(config, command, "--gradient_accumulation_steps", "gradient_accumulation_steps")
if not eval_dataset:
_optional_arg(config, command, "--val_size", "val_size") _optional_arg(config, command, "--val_size", "val_size")
_optional_arg(config, command, "--max_samples", "max_samples") _optional_arg(config, command, "--max_samples", "max_samples")
_optional_arg(config, command, "--preprocessing_num_workers", "preprocessing_num_workers") _optional_arg(config, command, "--preprocessing_num_workers", "preprocessing_num_workers")
@@ -293,4 +297,3 @@ def parse_log_line(line: str) -> dict[str, float] | None:
if match: if match:
result[key] = float(match.group(1)) result[key] = float(match.group(1))
return result or None return result or None

View File

@@ -0,0 +1,23 @@
from __future__ import annotations
from compute.engines.llama_factory.adapter import build_command
def test_build_command_uses_explicit_validation_dataset_without_resplitting() -> None:
result = build_command(
{
"base_model": "/models/qwen",
"dataset": "ygft_dataset_train",
"eval_dataset": "ygft_dataset_validation",
"dataset_dir": "/datasets/example",
"output_dir": "/outputs/example",
"val_size": 0.1,
}
)
assert result.command[result.command.index("--dataset") + 1] == "ygft_dataset_train"
assert result.command[result.command.index("--eval_dataset") + 1] == (
"ygft_dataset_validation"
)
assert "--do_eval" in result.command
assert "--val_size" not in result.command