mirror of https://github.com/razor-ai/soup.git
271 lines
9.9 KiB
Python
271 lines
9.9 KiB
Python
"""Tests for --tensorboard flag in soup train command."""
|
|
|
|
import re
|
|
from unittest.mock import MagicMock
|
|
from unittest.mock import patch as mock_patch
|
|
|
|
|
|
def _strip_ansi(text: str) -> str:
|
|
"""Remove ANSI escape codes from text."""
|
|
return re.sub(r'\x1b\[[0-9;]*m', '', text)
|
|
|
|
# ─── Flag Conflict Tests ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestTensorBoardFlagConflict:
|
|
"""Test that --wandb and --tensorboard cannot be used together."""
|
|
|
|
def test_wandb_and_tensorboard_conflict(self, tmp_path):
|
|
"""Should fail if both --wandb and --tensorboard are set."""
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
|
|
config_file = tmp_path / "soup.yaml"
|
|
config_file.write_text(
|
|
"base: some-model\n"
|
|
"task: sft\n"
|
|
"data:\n"
|
|
" train: ./data.jsonl\n"
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, [
|
|
"train", "--config", str(config_file),
|
|
"--wandb", "--tensorboard",
|
|
])
|
|
assert result.exit_code != 0
|
|
assert "cannot use" in result.output.lower() or "pick one" in result.output.lower()
|
|
|
|
def test_tensorboard_only_does_not_conflict(self, tmp_path):
|
|
"""--tensorboard alone should not trigger conflict error."""
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
|
|
config_file = tmp_path / "soup.yaml"
|
|
config_file.write_text(
|
|
"base: some-model\n"
|
|
"task: sft\n"
|
|
"data:\n"
|
|
" train: ./data.jsonl\n"
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, [
|
|
"train", "--config", str(config_file), "--tensorboard",
|
|
])
|
|
# Should not hit the conflict error — may fail later (import/data)
|
|
assert "cannot use" not in result.output.lower()
|
|
|
|
|
|
# ─── TensorBoard Import Check Tests ──────────────────────────────────────
|
|
|
|
|
|
class TestTensorBoardImportCheck:
|
|
"""Test that --tensorboard checks for tensorboard installation."""
|
|
|
|
def test_tensorboard_flag_checks_import(self, tmp_path):
|
|
"""Should fail with install hint when tensorboard is not available."""
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
|
|
config_file = tmp_path / "soup.yaml"
|
|
config_file.write_text(
|
|
"base: some-model\n"
|
|
"task: sft\n"
|
|
"data:\n"
|
|
" train: ./data.jsonl\n"
|
|
)
|
|
|
|
# Patch the specific import inside train.py to raise ImportError
|
|
with mock_patch(
|
|
"soup_cli.commands.train.typer",
|
|
wraps=__import__("typer"),
|
|
):
|
|
original_import = __builtins__["__import__"] if isinstance(
|
|
__builtins__, dict
|
|
) else __builtins__.__import__
|
|
|
|
def mock_import(name, *args, **kwargs):
|
|
if name == "tensorboard":
|
|
raise ImportError("no tensorboard")
|
|
return original_import(name, *args, **kwargs)
|
|
|
|
with mock_patch("builtins.__import__", side_effect=mock_import):
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, [
|
|
"train", "--config", str(config_file), "--tensorboard",
|
|
])
|
|
|
|
assert result.exit_code != 0
|
|
|
|
|
|
# ─── CLI Help Tests ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTensorBoardCLI:
|
|
"""Test tensorboard flag appears in train command help."""
|
|
|
|
def test_tensorboard_in_train_help(self):
|
|
"""Train command should show --tensorboard in help."""
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, ["train", "--help"])
|
|
assert result.exit_code == 0
|
|
clean = _strip_ansi(result.output)
|
|
assert "--tensorboard" in clean
|
|
|
|
def test_tensorboard_help_text(self):
|
|
"""--tensorboard help should mention TensorBoard."""
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, ["train", "--help"])
|
|
clean = _strip_ansi(result.output)
|
|
assert "tensorboard" in clean.lower()
|
|
|
|
|
|
# ─── Trainer report_to Integration Tests ─────────────────────────────────
|
|
|
|
|
|
class TestTensorBoardTrainerIntegration:
|
|
"""Test that trainers receive report_to='tensorboard' when flag is set."""
|
|
|
|
def test_sft_trainer_accepts_tensorboard_report_to(self):
|
|
"""SFT trainer should accept report_to='tensorboard'."""
|
|
from soup_cli.config.schema import SoupConfig
|
|
from soup_cli.trainer.sft import SFTTrainerWrapper
|
|
|
|
cfg = SoupConfig(
|
|
base="some-model",
|
|
task="sft",
|
|
data={"train": "./data.jsonl"},
|
|
)
|
|
wrapper = SFTTrainerWrapper(cfg, device="cpu", report_to="tensorboard")
|
|
assert wrapper.report_to == "tensorboard"
|
|
|
|
def test_dpo_trainer_accepts_tensorboard_report_to(self):
|
|
"""DPO trainer should accept report_to='tensorboard'."""
|
|
from soup_cli.config.schema import SoupConfig
|
|
from soup_cli.trainer.dpo import DPOTrainerWrapper
|
|
|
|
cfg = SoupConfig(
|
|
base="some-model",
|
|
task="dpo",
|
|
data={"train": "./data.jsonl"},
|
|
)
|
|
wrapper = DPOTrainerWrapper(cfg, device="cpu", report_to="tensorboard")
|
|
assert wrapper.report_to == "tensorboard"
|
|
|
|
def test_grpo_trainer_accepts_tensorboard_report_to(self):
|
|
"""GRPO trainer should accept report_to='tensorboard'."""
|
|
from soup_cli.config.schema import SoupConfig
|
|
from soup_cli.trainer.grpo import GRPOTrainerWrapper
|
|
|
|
cfg = SoupConfig(
|
|
base="some-model",
|
|
task="grpo",
|
|
data={"train": "./data.jsonl"},
|
|
)
|
|
wrapper = GRPOTrainerWrapper(cfg, device="cpu", report_to="tensorboard")
|
|
assert wrapper.report_to == "tensorboard"
|
|
|
|
def test_kto_trainer_accepts_tensorboard_report_to(self):
|
|
"""KTO trainer should accept report_to='tensorboard'."""
|
|
from soup_cli.config.schema import SoupConfig
|
|
from soup_cli.trainer.kto import KTOTrainerWrapper
|
|
|
|
cfg = SoupConfig(
|
|
base="some-model",
|
|
task="kto",
|
|
data={"train": "./data.jsonl"},
|
|
)
|
|
wrapper = KTOTrainerWrapper(cfg, device="cpu", report_to="tensorboard")
|
|
assert wrapper.report_to == "tensorboard"
|
|
|
|
|
|
# ─── Sweep Integration Test ──────────────────────────────────────────────
|
|
|
|
|
|
class TestTensorBoardSweepRouting:
|
|
"""Test tensorboard works through sweep routing."""
|
|
|
|
def test_sweep_run_single_with_tensorboard_report_to(self):
|
|
"""Sweep _run_single should pass report_to through to trainer."""
|
|
from soup_cli.config.schema import SoupConfig
|
|
|
|
cfg = SoupConfig(
|
|
base="some-model",
|
|
task="sft",
|
|
data={"train": "./data.jsonl"},
|
|
)
|
|
|
|
fake_dataset = {"train": [{"messages": [{"role": "user", "content": "Q"}]}]}
|
|
fake_result = {
|
|
"initial_loss": 1.0,
|
|
"final_loss": 0.5,
|
|
"total_steps": 10,
|
|
"duration_secs": 60.0,
|
|
"output_dir": "./output",
|
|
"duration": "1m",
|
|
}
|
|
fake_gpu_info = {"memory_total": "0 MB", "memory_total_bytes": 0}
|
|
|
|
with mock_patch("soup_cli.data.loader.load_dataset", return_value=fake_dataset), \
|
|
mock_patch("soup_cli.utils.gpu.detect_device", return_value=("cpu", "CPU")), \
|
|
mock_patch("soup_cli.utils.gpu.get_gpu_info", return_value=fake_gpu_info), \
|
|
mock_patch("soup_cli.experiment.tracker.ExperimentTracker") as mock_tracker_cls, \
|
|
mock_patch("soup_cli.monitoring.display.TrainingDisplay"), \
|
|
mock_patch("soup_cli.trainer.sft.SFTTrainerWrapper.setup"), \
|
|
mock_patch(
|
|
"soup_cli.trainer.sft.SFTTrainerWrapper.train", return_value=fake_result
|
|
):
|
|
mock_tracker = MagicMock()
|
|
mock_tracker.start_run.return_value = "run-tb-1"
|
|
mock_tracker_cls.return_value = mock_tracker
|
|
|
|
from soup_cli.commands.sweep import _run_single
|
|
|
|
result = _run_single(cfg, {}, "tb_run_1", None)
|
|
|
|
assert result["run_id"] == "run-tb-1"
|
|
|
|
|
|
# ─── Happy Path Test ─────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTensorBoardHappyPath:
|
|
"""Test that --tensorboard succeeds when tensorboard is installed."""
|
|
|
|
def test_tensorboard_accepted_when_installed(self, tmp_path):
|
|
"""--tensorboard should print 'enabled' when tensorboard is available."""
|
|
from typer.testing import CliRunner
|
|
|
|
from soup_cli.cli import app
|
|
|
|
config_file = tmp_path / "soup.yaml"
|
|
config_file.write_text(
|
|
"base: some-model\n"
|
|
"task: sft\n"
|
|
"data:\n"
|
|
" train: ./data.jsonl\n"
|
|
)
|
|
|
|
# Mock tensorboard import to succeed
|
|
mock_tb = MagicMock()
|
|
with mock_patch.dict("sys.modules", {"tensorboard": mock_tb}):
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, [
|
|
"train", "--config", str(config_file), "--tensorboard", "--yes",
|
|
])
|
|
# Should get past the tensorboard check — will fail later on
|
|
# data loading, but should print "TensorBoard logging enabled"
|
|
assert "tensorboard logging enabled" in result.output.lower()
|