Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions components/src/dynamo/profiler/profile_sla.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from dynamo.profiler.utils.config_modifiers.parallelization_mapping import (
PickedParallelConfig,
)
from dynamo.profiler.utils.config_modifiers.protocol import apply_dgd_overrides
from dynamo.profiler.utils.defaults import SearchStrategy
from dynamo.profiler.utils.dgd_generation import generate_dgd_config_with_planner
from dynamo.profiler.utils.dgdr_v1beta1_types import DynamoGraphDeploymentRequestSpec
Expand Down Expand Up @@ -279,6 +280,18 @@ async def run_profile(
else:
final_config = dgd_config

# --- Apply DGD overrides (user-supplied partial DGD) ---
if final_config and dgdr.overrides and dgdr.overrides.dgd:
if isinstance(final_config, list):
# only apply to the last element (the actual DGD)
# previous elements are generated config maps
final_config[-1] = apply_dgd_overrides(
final_config[-1], dgdr.overrides.dgd
)
Comment thread
hhzhang16 marked this conversation as resolved.
Resolved
elif isinstance(final_config, dict):
final_config = apply_dgd_overrides(final_config, dgdr.overrides.dgd)
logger.info("Applied DGD overrides to the final config.")

output_file = f"{ops.output_dir}/final_config.yaml"
if not final_config:
if ops.dry_run:
Expand Down
17 changes: 17 additions & 0 deletions components/src/dynamo/profiler/thorough.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from dynamo.profiler.utils.config import Config, get_service_name_by_type
from dynamo.profiler.utils.config_modifiers import CONFIG_MODIFIERS
from dynamo.profiler.utils.config_modifiers.protocol import apply_dgd_overrides
from dynamo.profiler.utils.dgdr_v1beta1_types import DynamoGraphDeploymentRequestSpec
from dynamo.profiler.utils.profile_common import ProfilerOperationalConfig
from dynamo.profiler.utils.profile_decode import get_num_request_range
Expand Down Expand Up @@ -86,6 +87,22 @@ async def run_thorough(
len(decode_candidates),
)

# --- Apply DGD overrides to each candidate ---
if dgdr.overrides and dgdr.overrides.dgd:
for candidate in prefill_candidates:
candidate.dgd_config = apply_dgd_overrides(
candidate.dgd_config, dgdr.overrides.dgd
)
for candidate in decode_candidates:
candidate.dgd_config = apply_dgd_overrides(
candidate.dgd_config, dgdr.overrides.dgd
)
logger.info(
"Applied DGD overrides to %d prefill + %d decode candidates.",
len(prefill_candidates),
len(decode_candidates),
)

config_modifier = CONFIG_MODIFIERS[backend]

# --- Stage 2: Benchmarking ---
Expand Down
94 changes: 94 additions & 0 deletions components/src/dynamo/profiler/utils/config_modifiers/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from __future__ import annotations

import copy
import logging
from typing import Any, Protocol, Tuple

Expand Down Expand Up @@ -624,3 +625,96 @@ def _apply_agg_worker(
cls._apply_worker_config(
cfg.spec.services[svc_name], agg_cli_args, agg_replicas, agg_gpus
)


# ---------------------------------------------------------------------------
# DGD override merging (module-level, backend-agnostic)
# ---------------------------------------------------------------------------

# Services whose CLI args are fully replaced by overrides.
# For engine-worker services (everything else), the main container args
# are *appended* because they contain profiler-generated sweep results.
_OVERRIDE_NON_WORKER_SERVICES = frozenset({"Frontend", "Planner"})
Comment thread
hhzhang16 marked this conversation as resolved.
Resolved

# The exact path suffix where profiler-generated CLI args live inside a
# service dict. Only this specific location gets append semantics.
_WORKER_ARGS_SUFFIX = ("extraPodSpec", "mainContainer", "args")


def _is_worker_main_container_args(path: list[str]) -> bool:
"""True when *path* is ``spec.services.<worker>.extraPodSpec.mainContainer.args``."""
if len(path) != 6:
return False
return (
path[0] == "spec"
and path[1] == "services"
and path[2] not in _OVERRIDE_NON_WORKER_SERVICES
and tuple(path[3:]) == _WORKER_ARGS_SUFFIX
)


def _deep_merge_overrides(
target: dict,
overrides: dict,
path: list[str],
) -> None:
"""Recursively merge *overrides* into *target* (mutates *target* in-place).

Rules:
- Dicts are merged recursively; missing intermediate keys are created.
- ``spec.services.<name>`` that does not exist in *target* is skipped
with a warning (all nested overrides under that service are dropped).
- Only ``spec.services.<worker>.extraPodSpec.mainContainer.args`` is
*appended* to the existing list (preserving profiler-generated CLI
args). ``args`` at any other path is replaced normally.
- All other leaf values replace the target value.
"""
for key, value in overrides.items():
current_path = path + [key]

# Guard: skip overrides for services that don't exist in the DGD
if (
len(current_path) == 3
and current_path[0] == "spec"
and current_path[1] == "services"
):
services = target.get("services", target) if path == ["spec"] else target
if key not in services:
logger.warning(
"Service '%s' does not exist in the generated DGD config; "
"overrides for this service will not be applied.",
key,
)
continue

if isinstance(value, dict) and isinstance(target.get(key), dict):
_deep_merge_overrides(target[key], value, current_path)
elif isinstance(value, dict) and key not in target:
target[key] = copy.deepcopy(value)
elif (
key == "args"
and isinstance(value, list)
and _is_worker_main_container_args(current_path)
):
existing = target.get(key) or []
target[key] = list(existing) + list(value)
else:
target[key] = (
copy.deepcopy(value) if isinstance(value, (dict, list)) else value
)


def apply_dgd_overrides(dgd_config: dict, overrides: dict) -> dict:
"""Deep-merge an ``overrides.dgd`` dict onto a generated DGD config.

Args:
dgd_config: The generated DynamoGraphDeployment config dict.
overrides: A partial DGD dict with the same structure. Leaf values
overwrite the corresponding keys in *dgd_config*.

Returns:
A new dict with the overrides applied (the original is not mutated).
"""
result = copy.deepcopy(dgd_config)
_deep_merge_overrides(result, overrides, path=[])
return result
26 changes: 26 additions & 0 deletions tests/profiler/configs/10_thorough_override_security_context.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Case 10: Thorough sweep with DGD overrides for imagePullSecrets.
# Verifies that overrides can inject new spec-level fields (imagePullSecrets)
# that do not exist in the base DGD template.
model: "Qwen/Qwen3-32B"
backend: trtllm
image: "nvcr.io/nvidia/dynamo:latest"
hardware:
gpuSku: h200_sxm
totalGpus: 8
numGpusPerNode: 8
workload:
isl: 4000
osl: 1000
sla:
ttft: 2000.0
itl: 50.0
Comment thread
tedzhouhk marked this conversation as resolved.
Resolved
searchStrategy: thorough
overrides:
dgd:
spec:
imagePullSecrets:
- name: my-registry-secret
- name: nvcr-pull-secret
55 changes: 55 additions & 0 deletions tests/profiler/configs/9a_thorough_dsr1_sglang_overrides.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Case 9a: Thorough sweep for DeepSeek-R1 on SGLang with DGD overrides.
# Overrides are derived by comparing the base sglang disagg template
# (examples/backends/sglang/deploy/disagg.yaml) with the DSR1 recipe
# (recipes/deepseek-r1/sglang/disagg-8gpu/deploy.yaml), excluding
# parallelization-mapping fields (--tp, --dp, --ep-size,
# --enable-dp-attention) which are swept by the profiler.
model: "deepseek-ai/DeepSeek-R1"
backend: sglang
image: "nvcr.io/nvidia/ai-dynamo/sglang-runtime:0.8.0"
hardware:
gpuSku: h200_sxm
totalGpus: 16
numGpusPerNode: 8
modelCache:
pvcName: model-cache
pvcMountPath: /opt/model
workload:
isl: 4000
osl: 1000
sla:
ttft: 2000.0
itl: 50.0
searchStrategy: thorough
overrides:
dgd:
spec:
services:
decode:
sharedMemory:
size: 80Gi
extraPodSpec:
mainContainer:
workingDir: /workspace
args:
- --mem-fraction-static
- "0.75"
- --prefill-round-robin-balance
- --watchdog-timeout
- "3600"
prefill:
sharedMemory:
size: 80Gi
extraPodSpec:
mainContainer:
workingDir: /workspace
args:
- --mem-fraction-static
- "0.75"
- --load-balance-method
- round_robin
- --watchdog-timeout
- "3600"
Loading
Loading