Skip to content

Commit 33b58f2

Browse files
committed
feat: add parallax serve command
1 parent 328c99f commit 33b58f2

3 files changed

Lines changed: 113 additions & 4 deletions

File tree

docs/user_guide/quick_start.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,22 +135,26 @@ For models such as Qwen3 and gpt-oss, the "reasoning" (or "thinking") feature is
135135

136136
### Skipping Scheduler
137137
Developers can start Parallax backend engine without a scheduler. Pipeline parallel start/end layers should be set manually.
138+
139+
For a standalone single-machine server:
140+
```sh
141+
parallax serve --m Qwen/Qwen3-0.6B
142+
```
143+
138144
An example of serving Qwen3-0.6B with 2-nodes:
139145
- First node:
140146
```sh
141-
python3 ./parallax/src/parallax/launch.py \
147+
parallax serve \
142148
--model-path Qwen/Qwen3-0.6B \
143149
--port 3000 \
144-
--max-batch-size 8 \
145150
--start-layer 0 \
146151
--end-layer 14
147152
```
148153
- Second node:
149154
```sh
150-
python3 ./parallax/src/parallax/launch.py \
155+
parallax serve \
151156
--model-path Qwen/Qwen3-0.6B \
152157
--port 3000 \
153-
--max-batch-size 8 \
154158
--start-layer 14 \
155159
--end-layer 28
156160
```

src/parallax/cli.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,32 @@ def join_command(args, passthrough_args: list[str] | None = None):
261261
_execute_with_graceful_shutdown(cmd, env=env)
262262

263263

264+
def serve_command(args, passthrough_args: list[str] | None = None):
265+
"""Start a standalone Parallax server by launching launch.py directly."""
266+
if not args.skip_upload:
267+
update_package_info()
268+
269+
check_python_version()
270+
271+
project_root = get_project_root()
272+
launch_script = project_root / "src" / "parallax" / "launch.py"
273+
274+
if not launch_script.exists():
275+
logger.info(f"Error: Launch script not found at {launch_script}")
276+
sys.exit(1)
277+
278+
env = os.environ.copy()
279+
env["SGLANG_ENABLE_JIT_DEEPGEMM"] = "0"
280+
281+
passthrough_args = passthrough_args or []
282+
cmd = [sys.executable, str(launch_script), "--model-path", args.model_path]
283+
284+
if passthrough_args:
285+
cmd.extend(passthrough_args)
286+
287+
_execute_with_graceful_shutdown(cmd, env=env)
288+
289+
264290
def chat_command(args, passthrough_args: list[str] | None = None):
265291
"""Start the Parallax chat server (equivalent to scripts/chat.sh)."""
266292
check_python_version()
@@ -358,6 +384,7 @@ def main():
358384
formatter_class=argparse.RawDescriptionHelpFormatter,
359385
epilog="""
360386
Examples:
387+
parallax serve --model-path Qwen/Qwen3-0.6B # Start standalone server
361388
parallax run # Start scheduler with frontend
362389
parallax run -m {model-name} -n {number-of-worker-nodes} # Start scheduler without frontend
363390
parallax run -m Qwen/Qwen3-0.6B -n 2 # example
@@ -369,6 +396,21 @@ def main():
369396

370397
subparsers = parser.add_subparsers(dest="command", help="Available commands")
371398

399+
# Add 'serve' command parser
400+
serve_parser = subparsers.add_parser(
401+
"serve", help="Start a standalone Parallax server by launching the model locally"
402+
)
403+
serve_parser.add_argument(
404+
"-m",
405+
"--model-path",
406+
required=True,
407+
type=str,
408+
help="Path to the model repository or model name",
409+
)
410+
serve_parser.add_argument(
411+
"-u", "--skip-upload", action="store_true", help="Skip upload package info"
412+
)
413+
372414
# Add 'run' command parser
373415
run_parser = subparsers.add_parser(
374416
"run", help="Start the Parallax scheduler (equivalent to scripts/start.sh)"
@@ -424,6 +466,8 @@ def main():
424466

425467
if args.command == "run":
426468
run_command(args, passthrough_args)
469+
elif args.command == "serve":
470+
serve_command(args, passthrough_args)
427471
elif args.command == "join":
428472
join_command(args, passthrough_args)
429473
elif args.command == "chat":

tests/test_cli.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from argparse import Namespace
2+
from pathlib import Path
3+
from unittest.mock import patch
4+
5+
from parallax import cli
6+
7+
8+
def test_serve_command_launches_local_server_without_scheduler(tmp_path):
9+
launch_script = tmp_path / "src" / "parallax" / "launch.py"
10+
launch_script.parent.mkdir(parents=True)
11+
launch_script.touch()
12+
13+
args = Namespace(model_path="Qwen/Qwen3-0.6B", skip_upload=True)
14+
15+
with (
16+
patch.object(cli, "check_python_version"),
17+
patch.object(cli, "get_project_root", return_value=Path(tmp_path)),
18+
patch.object(cli.sys, "executable", "/repo/.venv/bin/python"),
19+
patch.object(cli, "_execute_with_graceful_shutdown") as execute,
20+
):
21+
cli.serve_command(args, ["--log-level", "DEBUG", "--port", "3005"])
22+
23+
cmd = execute.call_args.args[0]
24+
env = execute.call_args.kwargs["env"]
25+
26+
assert cmd == [
27+
"/repo/.venv/bin/python",
28+
str(launch_script),
29+
"--model-path",
30+
"Qwen/Qwen3-0.6B",
31+
"--log-level",
32+
"DEBUG",
33+
"--port",
34+
"3005",
35+
]
36+
assert "--scheduler-addr" not in cmd
37+
assert env["SGLANG_ENABLE_JIT_DEEPGEMM"] == "0"
38+
39+
40+
def test_main_dispatches_serve_command_with_passthrough_args():
41+
with (
42+
patch.object(
43+
cli.sys,
44+
"argv",
45+
[
46+
"parallax",
47+
"serve",
48+
"--model-path",
49+
"Qwen/Qwen3-0.6B",
50+
"--log-level",
51+
"DEBUG",
52+
],
53+
),
54+
patch.object(cli, "serve_command") as serve_command,
55+
):
56+
cli.main()
57+
58+
args, passthrough_args = serve_command.call_args.args
59+
assert args.command == "serve"
60+
assert args.model_path == "Qwen/Qwen3-0.6B"
61+
assert passthrough_args == ["--log-level", "DEBUG"]

0 commit comments

Comments
 (0)