-
Notifications
You must be signed in to change notification settings - Fork 61
Add aggregate mode support to AIDynamo workload #982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0d78c4e
43fd178
9770f63
c9cae6f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,9 +162,17 @@ class AIDynamoArgs(BaseModel): | |
|
|
||
| model: str = "Qwen/Qwen3-0.6B" | ||
| backend: Literal["vllm", "sglang", "sglang_dsr1"] = "vllm" | ||
| mode: Literal["aggregate", "disaggregated"] = "disaggregated" | ||
| endpoint: str = Field(default="v1/chat/completions") | ||
| connector: Optional[str | list[str]] = None | ||
|
|
||
| @field_validator("mode", mode="before") | ||
| @classmethod | ||
| def normalize_mode(cls, v: str) -> str: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is too much of a syntax sugar |
||
| if v == "disaggregate": | ||
| return "disaggregated" | ||
| return v | ||
|
|
||
| @field_validator("connector", mode="before") | ||
| @classmethod | ||
| def validate_connector(cls, v: str | list[str] | None) -> str | list[str] | None: | ||
|
|
@@ -234,14 +242,21 @@ def validate_connector(cls, v: str | list[str] | None) -> str | list[str] | None | |
| @model_validator(mode="after") | ||
| def populate_prefill_decode_args(self) -> "AIDynamoArgs": | ||
| """Populate prefill/decode args.""" | ||
| if self.mode == "aggregate" and self.backend.lower() != "vllm": | ||
| raise ValueError("AI Dynamo aggregate mode is currently supported only for the vLLM backend") | ||
|
Comment on lines
+245
to
+246
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's omit this validation. if one day aggregate mode will be supported by sglang/trt-llm I don't want CloudAI code to be the blocker |
||
|
|
||
| if self.backend.lower() == "vllm": | ||
| self.prefill_worker.args.model = self.model | ||
| self.decode_worker.args.model = self.model | ||
| if self.mode == "aggregate": | ||
| self.prefill_worker.cmd = "python3 -m dynamo.vllm" | ||
| else: | ||
| self.decode_worker.args.model = self.model | ||
|
Comment on lines
+245
to
+253
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline src/cloudai/workloads/ai_dynamo/ai_dynamo.py --items all --type class,function --match 'AIDynamoArgs|populate_prefill_decode_args'
rg -n -C 4 -- '--is-prefill-worker|mode == "aggregate"|populate_prefill_decode_args' \
src/cloudai/workloads/ai_dynamo/ai_dynamo.py \
src/cloudai/workloads/ai_dynamo/ai_dynamo.sh \
tests/workloads/ai_dynamo/test_command_gen_strategy_slurm.pyRepository: NVIDIA/cloudai Length of output: 3918 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ai_dynamo.py relevant sections ---'
sed -n '150,270p' src/cloudai/workloads/ai_dynamo/ai_dynamo.py
sed -n '580,680p' src/cloudai/workloads/ai_dynamo/ai_dynamo.py
printf '%s\n' '--- command generation and Dynamo references ---'
rg -n -C 5 --glob '!*.lock' \
'prefill_worker|decode_worker|dynamo\.vllm|ai-dynamo|ai_dynamo|Dynamo|is-prefill-worker|aggregate' \
src tests pyproject.toml setup.cfg setup.py requirements*.txt Dockerfile* .github 2>/dev/null || trueRepository: NVIDIA/cloudai Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- AI Dynamo files ---'
git ls-files 'src/cloudai/workloads/ai_dynamo/*' 'tests/workloads/ai_dynamo/*'
printf '%s\n' '--- AI Dynamo outlines ---'
for f in $(git ls-files 'src/cloudai/workloads/ai_dynamo/*.py' 'tests/workloads/ai_dynamo/*.py'); do
echo "### $f"
ast-grep outline "$f" --items all --type class,function 2>/dev/null | head -n 120
done
printf '%s\n' '--- target model and worker logic ---'
sed -n '150,270p' src/cloudai/workloads/ai_dynamo/ai_dynamo.py
sed -n '600,660p' src/cloudai/workloads/ai_dynamo/ai_dynamo.py
printf '%s\n' '--- AI Dynamo command generation ---'
rg -n -C 8 --glob '*.py' --glob '*.sh' \
'AIDynamoSlurmCommandGenStrategy|prefill_worker|decode_worker|worker_cmd|is-prefill-worker|dynamo\.vllm|mode' \
src/cloudai/workloads/ai_dynamo tests/workloads/ai_dynamo
printf '%s\n' '--- version and image references ---'
rg -n -C 3 --glob '!*.lock' --glob '!*.json' \
'ai-dynamo|dynamo.*(commit|sha|tag|version)|DYNAMO|dynamo:v|dynamo_image|container.*image' \
.github Dockerfile* docs examples pyproject.toml setup.cfg setup.py requirements*.txt 2>/dev/null || trueRepository: NVIDIA/cloudai Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Slurm worker generation ---'
sed -n '1,220p' src/cloudai/workloads/ai_dynamo/slurm_command_gen_strategy.py
sed -n '220,520p' src/cloudai/workloads/ai_dynamo/slurm_command_gen_strategy.py
printf '%s\n' '--- aggregate tests and fixtures ---'
sed -n '1,130p' tests/workloads/ai_dynamo/test_command_gen_strategy_slurm.py
sed -n '420,490p' tests/workloads/ai_dynamo/test_command_gen_strategy_slurm.py
printf '%s\n' '--- aggregate references in AI Dynamo only ---'
rg -n -C 12 --glob '*.py' --glob '*.sh' \
'mode|aggregate|prefill_worker|decode_worker|is-prefill-worker|is-decode-worker' \
src/cloudai/workloads/ai_dynamo tests/workloads/ai_dynamo \
| awk 'length($0) < 500' | head -n 500Repository: NVIDIA/cloudai Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ai_dynamo.sh option parsing and launch logic ---'
rg -n -C 12 -- '--prefill-cmd|prefill_cmd|decode_cmd|mode|aggregate|dynamo\.vllm|is-prefill-worker|is-decode-worker' \
src/cloudai/workloads/ai_dynamo/ai_dynamo.sh
printf '%s\n' '--- local pin references ---'
rg -n -C 4 -- 'f7e468c7e8ff0d1426db987564e60572167e8464|github.com/ai-dynamo/dynamo' \
src tests examples docs .github 2>/dev/null | head -n 200
printf '%s\n' '--- upstream pinned files ---'
base='https://raw.githubusercontent.com/ai-dynamo/dynamo/f7e468c7e8ff0d1426db987564e60572167e8464'
for file in \
examples/backends/vllm/launch/agg_kvbm.sh \
examples/backends/vllm/launch/disagg_prefill.sh \
components/src/dynamo/vllm/main.py \
components/src/dynamo/vllm/args.py; do
echo "### $file"
curl -fsSL "$base/$file" | rg -n -C 5 -- 'is-prefill-worker|is-decode-worker|aggregate|dynamo\.vllm|launch' || true
doneRepository: NVIDIA/cloudai Length of output: 21409 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- worker command identifiers in ai_dynamo.sh ---'
rg -n -C 10 \
'prefill_config|decode_config|prefill_cmd|decode_cmd|prefill-cmd|decode-cmd|launch_.*worker|worker.*launch|srun.*dynamo|python.*dynamo|dynamo\.vllm' \
src/cloudai/workloads/ai_dynamo/ai_dynamo.sh || true
printf '%s\n' '--- pinned upstream file status and relevant lines ---'
base='https://raw.githubusercontent.com/ai-dynamo/dynamo/f7e468c7e8ff0d1426db987564e60572167e8464'
for file in \
examples/backends/vllm/launch/agg_kvbm.sh \
examples/backends/vllm/launch/disagg_prefill.sh \
components/src/dynamo/vllm/main.py \
components/src/dynamo/vllm/args.py \
components/src/dynamo/vllm/entrypoint.py \
components/src/dynamo/vllm/worker.py; do
tmp=$(mktemp)
status=$(curl -L -sS -o "$tmp" -w '%{http_code}' "$base/$file")
echo "### $file HTTP $status"
if [ "$status" = 200 ]; then
rg -n -C 6 -- 'is-prefill-worker|is-decode-worker|aggregate|dynamo\.vllm|launch' "$tmp" || true
fi
rm -f "$tmp"
doneRepository: NVIDIA/cloudai Length of output: 27477 🏁 Script executed: #!/bin/bash
set -u
sha='f7e468c7e8ff0d1426db987564e60572167e8464'
api="https://api.github.com/repos/ai-dynamo/dynamo/git/trees/${sha}?recursive=1"
tmp=$(mktemp)
status=$(curl -L -sS -o "$tmp" -w '%{http_code}' "$api")
echo "tree HTTP $status"
if [ "$status" = 200 ]; then
jq -r '.tree[] | select(.type=="blob") | .path' "$tmp" |
rg -i '(^|/)(vllm|launch|agg|aggregate|disagg|main|args|worker)(/|\.|$)' |
rg -i 'vllm|launch|agg|aggregate|disagg' |
head -n 300
else
head -c 1000 "$tmp"
fi
rm -f "$tmp"
printf '%s\n' '--- ai_dynamo.sh main launch sequence ---'
sed -n '1460,1620p' src/cloudai/workloads/ai_dynamo/ai_dynamo.shRepository: NVIDIA/cloudai Length of output: 4643 🏁 Script executed: #!/bin/bash
set -u
base='https://raw.githubusercontent.com/ai-dynamo/dynamo/f7e468c7e8ff0d1426db987564e60572167e8464'
for file in \
components/backends/vllm/launch/agg.sh \
components/backends/vllm/launch/disagg.sh \
components/backends/vllm/src/dynamo/vllm/args.py \
components/backends/vllm/src/dynamo/vllm/main.py; do
tmp=$(mktemp)
status=$(curl -L -sS -o "$tmp" -w '%{http_code}' "$base/$file")
echo "### $file HTTP $status"
if [ "$status" = 200 ]; then
rg -n -C 10 -- \
'is-prefill-worker|is-decode-worker|prefill|decode|aggregate|Dynamo|Worker' "$tmp" |
head -n 300 || true
else
head -c 500 "$tmp"
fi
rm -f "$tmp"
doneRepository: NVIDIA/cloudai Length of output: 14490 🏁 Script executed: #!/bin/bash
set -u
base='https://raw.githubusercontent.com/ai-dynamo/dynamo/f7e468c7e8ff0d1426db987564e60572167e8464'
tmp=$(mktemp)
curl -L -sS "$base/components/backends/vllm/launch/agg.sh" > "$tmp"
echo '--- pinned aggregate launch script ---'
sed -n '1,180p' "$tmp"
rm -f "$tmp"
echo '--- aggregate generated worker argument shape ---'
python3 - <<'PY'
from pathlib import Path
text = Path("src/cloudai/workloads/ai_dynamo/slurm_command_gen_strategy.py").read_text()
start = text.index(" def _gen_worker_script_args")
end = text.index(" def _gen_script_args", start)
branch = text[start:end]
print(branch)
print("aggregate returns prefill args before decode args:", "if td.cmd_args.dynamo.mode == \"aggregate\":" in branch)
print("prefill command is serialized by nested worker args:", '"prefill_worker"' not in branch.split("def _gen_worker_script_args", 1)[1].split("def _gen_script_args", 1)[0])
PYRepository: NVIDIA/cloudai Length of output: 1596 Launch a combined vLLM worker in aggregate mode. When 🧰 Tools🪛 Ruff (0.16.1)[warning] 246-246: Avoid specifying long messages outside the exception class (TRY003) 🤖 Prompt for AI Agents |
||
| elif self.backend.lower() in ["sglang", "sglang_dsr1"]: | ||
| self.prefill_worker.args.model_path = self.model | ||
| self.decode_worker.args.model_path = self.model | ||
| self.prefill_worker.args.served_model_name = self.model | ||
| self.decode_worker.args.served_model_name = self.model | ||
| if self.mode == "disaggregated": | ||
| self.decode_worker.args.model_path = self.model | ||
| self.decode_worker.args.served_model_name = self.model | ||
| else: | ||
| raise ValueError(f"Invalid backend: {self.backend}") | ||
|
|
||
|
|
@@ -593,19 +608,20 @@ def was_run_successful(self, tr: TestRun) -> JobStatusResult: | |
| return JobStatusResult(workloads_successful and accuracy_successful) | ||
|
|
||
| def constraint_check(self, tr: TestRun, system: Optional[System]) -> bool: | ||
| is_aggregate = tr.test.cmd_args.dynamo.mode == "aggregate" | ||
| prefill_worker = tr.test.cmd_args.dynamo.prefill_worker | ||
| decode_worker = tr.test.cmd_args.dynamo.decode_worker | ||
|
|
||
| prefill_tp = prefill_worker.args.tensor_parallel_size | ||
| prefill_pp = prefill_worker.args.pipeline_parallel_size | ||
|
|
||
| decode_tp = decode_worker.args.tensor_parallel_size | ||
| decode_pp = decode_worker.args.pipeline_parallel_size | ||
|
|
||
| if self.constraints.prefill_tp_le_decode_tp and prefill_tp > decode_tp: | ||
| logging.info("constraint_check failed for: prefill_tp_le_decode_tp") | ||
| return False | ||
| logging.info("constraint_check passed for: prefill_tp_le_decode_tp") | ||
| if not is_aggregate: | ||
| if self.constraints.prefill_tp_le_decode_tp and prefill_tp > decode_tp: | ||
| logging.info("constraint_check failed for: prefill_tp_le_decode_tp") | ||
| return False | ||
| logging.info("constraint_check passed for: prefill_tp_le_decode_tp") | ||
|
|
||
| gpus_per_node = 0 | ||
| slurm_system = cast(SlurmSystem, system) | ||
|
|
@@ -615,12 +631,24 @@ def constraint_check(self, tr: TestRun, system: Optional[System]) -> bool: | |
| if ( | ||
| gpus_per_node > 0 | ||
| and self.constraints.tp_times_pp_le_gpus_per_node | ||
| and (prefill_tp * prefill_pp > gpus_per_node or decode_tp * decode_pp > gpus_per_node) | ||
| and prefill_tp * prefill_pp > gpus_per_node | ||
| ): | ||
| logging.info("constraint_check failed for: tp_times_pp_le_gpus_per_node") | ||
| return False | ||
| if ( | ||
| not is_aggregate | ||
| and gpus_per_node > 0 | ||
| and self.constraints.tp_times_pp_le_gpus_per_node | ||
| and decode_tp * decode_pp > gpus_per_node | ||
| ): | ||
| logging.info("constraint_check failed for: tp_times_pp_le_gpus_per_node") | ||
| return False | ||
| logging.info("constraint_check passed for: tp_times_pp_le_gpus_per_node") | ||
|
|
||
| if is_aggregate: | ||
| logging.info("constraint_check skipped split-worker checks: aggregate mode has no decode worker") | ||
| return True | ||
|
|
||
| role_total_nodes = int(prefill_worker.num_nodes) + int(decode_worker.num_nodes) | ||
| prefill_nodes = set(prefill_worker.nodes.split(",")) if prefill_worker.nodes else set() | ||
| decode_nodes = set(decode_worker.nodes.split(",")) if decode_worker.nodes else set() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just
agg/disagg? this will also eliminate the potential confusion between-eand-edsuffix (apparently your typing hasaggregatEbutdisaggregatED)