feat: unify Docker serving configuration in YAML
- Add server.py --config serve.yaml; explicit CLI flags override YAML - Add scripts/serve.sh and serve_runtime.py for the Compose lifecycle - Template server/cpu ports and param mounts in docker-compose.yml - Document schema in docs/developer/docker-serving.md and params guide - Add tests for runtime parsing and server CLI merge logic
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""Unit tests for the serving CLI YAML merge logic."""
|
||||
|
||||
import click
|
||||
import pytest
|
||||
import torch
|
||||
from click.testing import CliRunner
|
||||
|
||||
from scripts.tools.server import (
|
||||
_merge_yaml_into_kwargs,
|
||||
_resolve_server_config,
|
||||
server_command,
|
||||
)
|
||||
|
||||
|
||||
def _passed() -> dict:
|
||||
return {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8000,
|
||||
"reload": False,
|
||||
"param_path": None,
|
||||
"device": "cuda",
|
||||
"dtype": "bfloat16",
|
||||
"max_batch_size": 16,
|
||||
"max_seq_len": None,
|
||||
}
|
||||
|
||||
|
||||
def test_yaml_overrides_click_defaults_but_not_explicit_cli(tmp_path):
|
||||
config_path = tmp_path / "serve.yaml"
|
||||
config_path.write_text(
|
||||
"server:\n device: cpu\n dtype: float16\n max_batch_size: 8\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
merged = _merge_yaml_into_kwargs(
|
||||
str(config_path), _passed(), explicit_keys={"device"}
|
||||
)
|
||||
|
||||
assert merged["device"] == "cuda"
|
||||
assert merged["dtype"] == "float16"
|
||||
assert merged["max_batch_size"] == 8
|
||||
|
||||
|
||||
def test_resolve_config_yaml_wins_by_default(tmp_path):
|
||||
config_path = tmp_path / "serve.yaml"
|
||||
config_path.write_text(
|
||||
"server:\n port: 9000\n max_seq_len: 2048\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
resolved = _resolve_server_config(str(config_path), _passed())
|
||||
|
||||
assert resolved["port"] == 9000
|
||||
assert resolved["max_seq_len"] == 2048
|
||||
assert resolved["device"] == "cuda"
|
||||
assert resolved["dtype"] == "bfloat16"
|
||||
|
||||
|
||||
def test_resolve_config_rejects_bad_dtype(tmp_path):
|
||||
config_path = tmp_path / "serve.yaml"
|
||||
config_path.write_text("server:\n dtype: fp8\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(click.UsageError, match="server.dtype"):
|
||||
_resolve_server_config(str(config_path), _passed())
|
||||
|
||||
|
||||
def test_server_command_rejects_bad_yaml_dtype(tmp_path):
|
||||
config_path = tmp_path / "serve.yaml"
|
||||
config_path.write_text("server:\n dtype: fp8\n", encoding="utf-8")
|
||||
|
||||
result = CliRunner().invoke(server_command, ["--config", str(config_path)])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "server.dtype" in result.output
|
||||
|
||||
|
||||
def test_server_command_merges_yaml_and_cli(tmp_path, monkeypatch):
|
||||
"""Full CLI path: YAML values apply, explicit CLI flags override, args reach run_server."""
|
||||
config_path = tmp_path / "serve.yaml"
|
||||
config_path.write_text(
|
||||
"server:\n device: cpu\n dtype: float16\n max_batch_size: 8\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
captured = {}
|
||||
|
||||
def fake_run_server(**kwargs):
|
||||
captured.update(kwargs)
|
||||
|
||||
monkeypatch.setattr("scripts.tools.server.run_server", fake_run_server)
|
||||
result = CliRunner().invoke(
|
||||
server_command,
|
||||
["--config", str(config_path), "--max_batch_size", "32"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["device"] == "cpu"
|
||||
assert captured["dtype"] == torch.float16
|
||||
assert captured["max_batch_size"] == 32
|
||||
assert captured["port"] == 8000
|
||||
|
||||
|
||||
def test_config_option_rejects_missing_file(tmp_path):
|
||||
result = CliRunner().invoke(
|
||||
server_command, ["--config", str(tmp_path / "nope.yaml")]
|
||||
)
|
||||
|
||||
assert result.exit_code == 2
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Unit tests for the serving runtime configuration parser."""
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.tools.serve_runtime import load_runtime
|
||||
|
||||
|
||||
def _write(tmp_path, body: str) -> str:
|
||||
config_path = tmp_path / "serve.yaml"
|
||||
config_path.write_text(body, encoding="utf-8")
|
||||
return str(config_path)
|
||||
|
||||
|
||||
def test_runtime_exports_defaults(tmp_path):
|
||||
config_path = _write(
|
||||
tmp_path,
|
||||
"runtime:\n"
|
||||
" port: 8000\n"
|
||||
" paths:\n"
|
||||
" param: ./params\n"
|
||||
"server:\n"
|
||||
" device: cuda\n",
|
||||
)
|
||||
runtime = load_runtime(config_path)
|
||||
assert runtime["SERVE_PORT"] == "8000"
|
||||
assert runtime["SERVE_CONTAINER_PORT"] == "8000"
|
||||
assert runtime["SERVE_PARAM_DIR"] == str((tmp_path / "params").resolve())
|
||||
assert runtime["SERVE_GPU_ENABLED"] == "true"
|
||||
assert runtime["CUDA_VISIBLE_DEVICES"] == ""
|
||||
assert runtime["SERVE_DEVICE"] == "cuda"
|
||||
assert runtime["CUDA_TAG"] == "cu128"
|
||||
assert runtime["SERVE_JOB_NAME"] == ""
|
||||
|
||||
|
||||
def test_runtime_gpu_disabled_requires_cpu(tmp_path):
|
||||
config_path = _write(
|
||||
tmp_path,
|
||||
"runtime:\n gpu:\n enabled: false\nserver:\n device: cuda\n",
|
||||
)
|
||||
with pytest.raises(ValueError, match="server.device must be 'cpu'"):
|
||||
load_runtime(config_path)
|
||||
|
||||
|
||||
def test_runtime_gpu_devices_single_and_ports(tmp_path):
|
||||
config_path = _write(
|
||||
tmp_path,
|
||||
"runtime:\n"
|
||||
" gpu:\n"
|
||||
" devices: [1]\n"
|
||||
" port: 8080\n"
|
||||
"server:\n"
|
||||
" port: 9000\n"
|
||||
" device: cuda\n",
|
||||
)
|
||||
runtime = load_runtime(config_path)
|
||||
assert runtime["SERVE_PORT"] == "8080"
|
||||
assert runtime["SERVE_CONTAINER_PORT"] == "9000"
|
||||
assert runtime["CUDA_VISIBLE_DEVICES"] == "1"
|
||||
|
||||
|
||||
def test_runtime_gpu_devices_rejects_multi(tmp_path):
|
||||
config_path = _write(
|
||||
tmp_path,
|
||||
"runtime:\n gpu:\n devices: [0, 1]\n",
|
||||
)
|
||||
with pytest.raises(ValueError, match="single-device"):
|
||||
load_runtime(config_path)
|
||||
|
||||
|
||||
def test_runtime_port_out_of_range(tmp_path):
|
||||
config_path = _write(tmp_path, "runtime:\n port: 70000\n")
|
||||
with pytest.raises(ValueError, match="between 1 and 65535"):
|
||||
load_runtime(config_path)
|
||||
|
||||
|
||||
def test_runtime_environment_export(tmp_path):
|
||||
config_path = _write(
|
||||
tmp_path,
|
||||
"runtime:\n environment:\n TOKENIZERS_PARALLELISM: 'false'\n",
|
||||
)
|
||||
runtime = load_runtime(config_path)
|
||||
assert runtime["environment"] == {"TOKENIZERS_PARALLELISM": "false"}
|
||||
Reference in New Issue
Block a user