Skip to content

Commit 594c2d5

Browse files
committed
feat(operator): add single-pod Bulwark GMS workers
Add Docker-container-oriented intra-pod Bulwark worker generation with frontend gateway, primary/shadow engines, separate GMS weights and KV servers, discovery/probe handling, and operator validation tests. Local rootfs runner support is intentionally excluded from this public branch. Signed-off-by: Maksim Khadkevich <mkhadkevich@nvidia.com>
1 parent 0074685 commit 594c2d5

17 files changed

Lines changed: 940 additions & 163 deletions

components/src/dynamo/frontend/frontend_args.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class FrontendConfig(RouterConfigBase, KvRouterConfigBase, AicPerfConfigBase):
6161

6262
namespace: Optional[str] = None
6363
namespace_prefix: Optional[str] = None
64+
bulwark_gateway_endpoint: Optional[str] = None
6465

6566
migration_limit: int
6667
migration_max_seq_len: Optional[int]
@@ -93,6 +94,24 @@ def validate(self) -> None:
9394
self.router_mode = "kv"
9495
self.apply_load_aware_preset()
9596

97+
if self.bulwark_gateway_endpoint:
98+
if not self.bulwark_gateway_endpoint.startswith("dyn://"):
99+
raise ValueError(
100+
"--bulwark-gateway-endpoint must be a dyn://namespace.component.endpoint path"
101+
)
102+
if not self.namespace and not self.namespace_prefix:
103+
raise ValueError(
104+
"--bulwark-gateway-endpoint requires --namespace or --namespace-prefix "
105+
"to select the private primary/shadow worker namespace"
106+
)
107+
if self.interactive:
108+
raise ValueError(
109+
"--bulwark-gateway-endpoint cannot be combined with --interactive"
110+
)
111+
if self.kserve_grpc_server:
112+
raise ValueError(
113+
"--bulwark-gateway-endpoint cannot be combined with --kserve-grpc-server"
114+
)
96115
if bool(self.tls_cert_path) ^ bool(self.tls_key_path): # ^ is XOR
97116
raise ValueError(
98117
"--tls-cert-path and --tls-key-path must be provided together"
@@ -257,6 +276,19 @@ def add_arguments(self, parser) -> None:
257276
),
258277
)
259278

279+
add_argument(
280+
g,
281+
flag_name="--bulwark-gateway-endpoint",
282+
env_var="DYN_BULWARK_GATEWAY_ENDPOINT",
283+
default=None,
284+
help=(
285+
"Expose this frontend as a stable request-plane worker endpoint for Bulwark "
286+
"compound workers. The value is a public dyn://namespace.component.endpoint "
287+
"path; private primary/shadow workers are discovered using --namespace or "
288+
"--namespace-prefix."
289+
),
290+
)
291+
260292
add_argument(
261293
g,
262294
flag_name="--migration-limit",

components/src/dynamo/frontend/main.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,18 @@ async def async_main():
168168
Initializes the distributed runtime, configures routing, and starts
169169
the HTTP server or interactive mode based on command-line arguments.
170170
"""
171-
# The system status server port is a worker concern.
171+
# The system status server port is usually a worker concern.
172172
#
173173
# Serve tests set DYN_SYSTEM_PORT for the worker, but aggregated launch scripts
174-
# start `dynamo.frontend` first. If the frontend inherits DYN_SYSTEM_PORT, it can
175-
# bind that port before the worker, causing port conflicts and/or scraping the
176-
# wrong metrics endpoint.
174+
# start `dynamo.frontend` first. If the normal frontend inherits DYN_SYSTEM_PORT,
175+
# it can bind that port before the worker, causing port conflicts and/or scraping
176+
# the wrong metrics endpoint. Bulwark gateway endpoint mode intentionally keeps
177+
# the system server so Kubernetes readiness can gate the stable worker entity.
178+
frontend_system_port = os.environ.get("DYN_SYSTEM_PORT")
177179
os.environ.pop("DYN_SYSTEM_PORT", None)
178180
config, vllm_flags, sglang_flags = parse_args()
181+
if config.bulwark_gateway_endpoint and frontend_system_port:
182+
os.environ["DYN_SYSTEM_PORT"] = frontend_system_port
179183
dump_config(config.dump_config_to, config)
180184
if config.event_plane:
181185
os.environ["DYN_EVENT_PLANE"] = config.event_plane
@@ -192,8 +196,9 @@ async def async_main():
192196
f"Request migration {'enabled' if config.migration_limit > 0 else 'disabled'} "
193197
f"(limit: {config.migration_limit}{max_seq_info})"
194198
)
195-
# Warn if DYN_SYSTEM_PORT is set (frontend doesn't use system metrics server)
196-
if os.environ.get("DYN_SYSTEM_PORT"):
199+
# Warn if DYN_SYSTEM_PORT is set for a normal frontend. Gateway endpoint mode
200+
# uses it for Kubernetes readiness.
201+
if os.environ.get("DYN_SYSTEM_PORT") and not config.bulwark_gateway_endpoint:
197202
logger.warning(
198203
"=" * 80 + "\n"
199204
"WARNING: DYN_SYSTEM_PORT is set but NOT used by the frontend!\n"
@@ -307,7 +312,15 @@ def signal_handler():
307312
engine = await make_engine(runtime, e)
308313

309314
try:
310-
if config.interactive:
315+
if config.bulwark_gateway_endpoint:
316+
logger.info(
317+
"Starting Bulwark frontend gateway at %s; private workers selected by namespace=%s namespace_prefix=%s",
318+
config.bulwark_gateway_endpoint,
319+
config.namespace,
320+
config.namespace_prefix,
321+
)
322+
await run_input(runtime, config.bulwark_gateway_endpoint, engine)
323+
elif config.interactive:
311324
await run_input(runtime, "text", engine)
312325
elif config.kserve_grpc_server:
313326
await run_input(runtime, "grpc", engine)

deploy/operator/internal/consts/consts.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,13 @@ const (
103103
// corresponding node label onto the pod after scheduling.
104104
KubeAnnotationTopologyLabelKey = "nvidia.com/topology-label-key"
105105

106-
DynamoDeploymentConfigEnvVar = "DYN_DEPLOYMENT_CONFIG"
107-
DynamoNamespaceEnvVar = "DYN_NAMESPACE"
108-
DynamoNamespacePrefixEnvVar = "DYN_NAMESPACE_PREFIX"
109-
DynamoNamespaceWorkerSuffixEnvVar = "DYN_NAMESPACE_WORKER_SUFFIX"
110-
DynamoComponentEnvVar = "DYN_COMPONENT"
111-
DynamoDiscoveryBackendEnvVar = "DYN_DISCOVERY_BACKEND"
106+
DynamoDeploymentConfigEnvVar = "DYN_DEPLOYMENT_CONFIG"
107+
DynamoNamespaceEnvVar = "DYN_NAMESPACE"
108+
DynamoNamespacePrefixEnvVar = "DYN_NAMESPACE_PREFIX"
109+
DynamoNamespaceWorkerSuffixEnvVar = "DYN_NAMESPACE_WORKER_SUFFIX"
110+
DynamoComponentEnvVar = "DYN_COMPONENT"
111+
DynamoDiscoveryBackendEnvVar = "DYN_DISCOVERY_BACKEND"
112+
DynamoDiscoveryLogicalInstanceKeyEnvVar = "DYN_DISCOVERY_LOGICAL_INSTANCE_KEY"
112113

113114
// DynamoOperatorAllowGMSSnapshotEnvVar enables the temporary internal
114115
// GMS + Snapshot admission gate when set to "1".

deploy/operator/internal/dynamo/backend_sglang.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ func (b *SGLangBackend) UpdateContainer(container *corev1.Container, numberOfNod
3737
"next-steps", "upstream SGLang changes needed")
3838
}
3939

40+
if component.IsInterPodGMSEnabled() {
41+
if !containerHasGMSLoadFormat(container) {
42+
injectFlagsIntoContainerCommand(container, "--load-format gms", false, "sglang")
43+
}
44+
}
45+
4046
// For single node, nothing to do
4147
if numberOfNodes <= 1 {
4248
return

deploy/operator/internal/dynamo/backend_trtllm.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ func (b *TRTLLMBackend) UpdateContainer(container *corev1.Container, numberOfNod
3030
"next-steps", "upstream TensorRT-LLM changes needed")
3131
}
3232

33+
if component.IsInterPodGMSEnabled() {
34+
if !containerHasGMSLoadFormat(container) {
35+
injectFlagsIntoContainerCommand(container, "--load-format gms", false, "trtllm")
36+
}
37+
}
38+
3339
// For single node, nothing to do
3440
if numberOfNodes <= 1 {
3541
return
@@ -141,7 +147,7 @@ func (b *TRTLLMBackend) setupLeaderContainer(container *corev1.Container, number
141147
"cp /ssh-pk/private.key.pub $HOME/.ssh/authorized_keys",
142148
"chmod 600 $HOME/.ssh/id_rsa $HOME/.ssh/authorized_keys",
143149
"chmod 644 $HOME/.ssh/id_rsa.pub",
144-
fmt.Sprintf("printf 'Host *\\nIdentityFile '$HOME'/.ssh/id_rsa\\nStrictHostKeyChecking no\\nPort %d\\n' > $HOME/.ssh/config", commonconsts.MpiRunSshPort),
150+
fmt.Sprintf("printf 'Host *\\nIdentityFile '$HOME'/.ssh/id_rsa\\nStrictHostKeyChecking no\\nUserKnownHostsFile /dev/null\\nGlobalKnownHostsFile /dev/null\\nPort %d\\n' > $HOME/.ssh/config", commonconsts.MpiRunSshPort),
145151
}
146152

147153
// Calculate total number of GPUs across all nodes. In the normal pod

0 commit comments

Comments
 (0)