Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ llmdbenchmark --version
| `-p NS` | `LLMDBENCH_NAMESPACE` | Namespace(s) to render into the plan |
| `-m MODELS` | `LLMDBENCH_MODELS` | Model to render the plan for |
| `-t METHODS` | `LLMDBENCH_METHODS` | Deployment method (`standalone`, `modelservice`) |
| `--gateway-class CLASS` | `LLMDBENCH_GATEWAY_CLASS` | Override the scenario's `gateway.className`. Accepted on the modelservice path: `epponly`, `istio`, `agentgateway`, `gke`, `data-science-gateway-class`. Ignored (any value accepted, including `none`) when the active deploy method is `kustomize`, `standalone`, or `fma`. |
| `--gateway-class CLASS` | `LLMDBENCH_GATEWAY_CLASS` | Override the scenario's `gateway.className`. Accepted on the modelservice path: `none`, `epponly`, `istio`, `agentgateway`, `gke`, `data-science-gateway-class`. `none` exposes decode vLLM directly through a plain Service with no Gateway, EPP, Envoy, or routing proxy. Ignored when the active deploy method is `kustomize`, `standalone`, or `fma`. |
| `-f` / `--monitoring` | | Enable monitoring in rendered templates (PodMonitor, EPP verbosity) |
| `-k FILE` | `LLMDBENCH_KUBECONFIG` / `KUBECONFIG` | Kubeconfig path (used for cluster resource auto-detection) |

Expand Down
8 changes: 5 additions & 3 deletions config/templates/jinja/08_httproute.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
`siblingStacks` and `stackIndex` are injected by
llmdbenchmark.parser.render_plans._process_stack.
#}
{# Modelservice-only template. Two skip conditions:
{# Modelservice-only template. Skip when:
1. -t kustomize -- upstream guide manifests define their own routing.
2. gateway.className == 'epponly' -- no Gateway exists, so an HTTPRoute
would be unresolvable; clients hit the EPP service directly. #}
would be unresolvable; clients hit the EPP service directly.
3. gateway.className == 'none' -- clients hit the model server's plain
Service directly; no Gateway, router, or HTTPRoute is deployed. #}
{% if standalone.enabled is defined and not standalone.enabled
and not (kustomize.enabled | default(false))
and gateway.className | default('') != 'epponly' %}
and gateway.className | default('') not in ['epponly', 'none'] %}
{% set _mode = (httpRoute.mode if httpRoute is defined and httpRoute.mode is defined else 'per-stack') %}
{% if _mode == 'shared' %}
{% set _owner_index = sharedInfraStackIndex | default(1) | int %}
Expand Down
11 changes: 9 additions & 2 deletions config/templates/jinja/10_helmfile-main.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ helmDefaults:
ghcr.io/llm-d/charts. The scenario layer uses the chart's `router.*`
keys directly -- see 12_router-values.yaml.j2 for the renderer. #}
{% set _epponly = (gateway.className | default('') == 'epponly') %}
{% set _direct = (gateway.className | default('') == 'none') %}
{% set _gaie_chart = 'oci://ghcr.io/llm-d/charts/llm-d-router-standalone' if _epponly else 'oci://ghcr.io/llm-d/charts/llm-d-router-gateway' %}

repositories:
- name: llm-d-modelservice
url: {{ helmRepositories.llmDModelservice.url }}
{% if not _direct %}
- name: llm-d-infra
url: {{ helmRepositories.llmDInfra.url }}
{% endif %}

releases:
{# `infra-llmdbench` (llm-d-infra chart -> Gateway resource + CRDs) is scenario-
Expand All @@ -44,7 +47,7 @@ releases:
`epponly` skips this release entirely - there is no Gateway resource. #}
{% set _is_shared_infra_owner = (stackIndex is not defined or stackIndex | int == sharedInfraStackIndex | default(1) | int) %}
{% set _is_first_stack = _is_shared_infra_owner %}
{% if _is_first_stack and not _epponly %}
{% if _is_first_stack and not _epponly and not _direct %}
- name: infra-llmdbench
namespace: {{ gateway.namespace }}
chart: llm-d-infra/llm-d-infra
Expand All @@ -62,16 +65,19 @@ releases:
chart: llm-d-modelservice/llm-d-modelservice
version: {{ chartVersions.llmDModelservice }}
installed: true
{% if not _direct %}
needs:
{% if _is_first_stack and not _epponly %}
{% if _is_first_stack and not _epponly and not _direct %}
- {{ gateway.namespace }}/infra-llmdbench
{% endif %}
- {{ gateway.namespace }}/{{ model_id_label }}-router
{% endif %}
values:
- ms-values.yaml
labels:
kind: inference-stack

{% if not _direct %}
- name: {{ model_id_label }}-router
namespace: {{ gateway.namespace }}
chart: {{ _gaie_chart }}
Expand All @@ -89,3 +95,4 @@ releases:
labels:
kind: inference-stack
{% endif %}
{% endif %}
5 changes: 3 additions & 2 deletions config/templates/jinja/11_infra.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
{% if standalone.enabled is defined and not standalone.enabled and not (kustomize.enabled | default(false)) %}
{% set gw_class = gateway.className %}
{# `epponly` renders no infra values: no Gateway is installed, the standalone
router chart provides HTTP service directly via the EPP Envoy sidecar. #}
{% if gw_class == 'epponly' %}
router chart provides HTTP service directly via the EPP Envoy sidecar.
`none` also renders no infra values because clients use a plain Service. #}
{% if gw_class in ['epponly', 'none'] %}
{% elif gw_class == 'gke' %}
{# ── GKE: managed gateway with destinationRule ── #}
gateway:
Expand Down
31 changes: 31 additions & 0 deletions config/templates/jinja/13a_modelservice-direct-service.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{# Modelservice direct baseline: expose decode vLLM without Gateway, EPP,
Envoy, or the routing proxy. RenderPlans forces routing.proxy.enabled=false
for this mode, so vLLM binds directly to routing.servicePort. #}
{% set decode_create = (decode.enabled if decode.enabled is defined else (decode.replicas | default(0) | int > 0)) %}
{% if standalone.enabled is defined and not standalone.enabled
and not (kustomize.enabled | default(false))
and gateway.className | default('') == 'none'
and decode_create %}
apiVersion: v1
kind: Service
metadata:
name: {{ model_id_label }}-direct
namespace: {{ namespace.name }}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WenjiaoYue is this change intended?

labels:
app.kubernetes.io/managed-by: llm-d-benchmark
llm-d.ai/model: {{ model_id_label }}
llm-d.ai/role: decode
stood-up-from: llm-d-benchmark
stood-up-via: modelservice
spec:
type: ClusterIP
selector:
llm-d.ai/model: {{ model_id_label }}
llm-d.ai/role: decode
ports:
- name: http
appProtocol: http
port: {{ routing.servicePort }}
targetPort: {{ routing.servicePort }}
protocol: TCP
{% endif %}
2 changes: 2 additions & 0 deletions config/templates/values/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ gateway:
# Kubernetes Gateway is deployed, the EPP
# runs with an Envoy sidecar that serves
# HTTP directly (single-stack only).
# none - plain ClusterIP Service to decode vLLM;
# no Gateway, EPP, Envoy, or routing proxy.
className: istio
providerNamespace: istio-system
# Gateway version is derived from chartVersions.istiod at plan time
Expand Down
6 changes: 5 additions & 1 deletion docs/standup.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,22 @@ The scenario parameters can be roughly categorized in four groups:

| Variable | Meaning | Note |
| -------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| LLMDBENCH_VLLM_MODELSERVICE_GATEWAY_CLASS_NAME | Gateway implementation used for the inference gateway | Default=`istio`. Supported: `istio`, `agentgateway`, `gke`, `data-science-gateway-class`, `epponly`. |
| LLMDBENCH_VLLM_MODELSERVICE_GATEWAY_CLASS_NAME | Gateway implementation used for the inference gateway | Default=`istio`. Supported: `none`, `istio`, `agentgateway`, `gke`, `data-science-gateway-class`, `epponly`. |

Gateway class options (set via `gateway.className` in the scenario YAML):

| `className` | What it deploys | Use when |
|------------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| `none` | ModelService decode pods plus a plain ClusterIP Service; **no** Gateway, HTTPRoute, EPP, Envoy, or routing proxy | Measuring direct model-server performance and routing overhead |
| `istio` (default) | istio-base + istiod control plane, a Gateway + HTTPRoute, the `llm-d-router-gateway-dev` chart | Default; most flexible / production deployments |
| `agentgateway` | agentgateway-crds + agentgateway controller, a Gateway + HTTPRoute, the `llm-d-router-gateway-dev` chart | Want agentgateway's data plane instead of Envoy/Istio |
| `gke` | Uses GKE-managed Gateway controller; same `llm-d-router-gateway-dev` chart | Running on GKE |
| `data-science-gateway-class` | OpenDataHub / OpenShift AI managed Gateway | Running on OpenShift AI |
| `epponly` | **No** Kubernetes Gateway, **no** HTTPRoute, the `llm-d-router-standalone-dev` chart (EPP with an Envoy sidecar serving HTTP) | You want llm-d's standalone router topology without any gateway |

`none` is a baseline lane, not a routing topology. It requires at least one
decode replica and does not support P/D disaggregation.

### Overriding `gateway.className` from the CLI

Every subcommand that renders templates (`plan`, `standup`, `experiment`,
Expand Down
3 changes: 2 additions & 1 deletion llmdbenchmark/interface/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ def add_subcommands(
default=env("LLMDBENCH_GATEWAY_CLASS"),
help=(
"Override the scenario's gateway.className. Supported values: "
"epponly, istio, agentgateway, gke, data-science-gateway-class. "
"none, epponly, istio, agentgateway, gke, "
"data-science-gateway-class. "
"Only takes effect on the modelservice deploy path."
),
)
Expand Down
3 changes: 2 additions & 1 deletion llmdbenchmark/interface/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def add_subcommands(
default=env("LLMDBENCH_GATEWAY_CLASS"),
help=(
"Override the scenario's gateway.className. Supported values: "
"epponly, istio, agentgateway, gke, data-science-gateway-class. "
"none, epponly, istio, agentgateway, gke, "
"data-science-gateway-class. "
"Only takes effect on the modelservice deploy path."
),
)
Expand Down
3 changes: 2 additions & 1 deletion llmdbenchmark/interface/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def add_subcommands(
help=(
"Override the scenario's gateway.className when the run phase "
"re-renders templates for setup overrides. Supported values: "
"epponly, istio, agentgateway, gke, data-science-gateway-class."
"none, epponly, istio, agentgateway, gke, "
"data-science-gateway-class."
),
)
run_parser.add_argument(
Expand Down
3 changes: 2 additions & 1 deletion llmdbenchmark/interface/standup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def add_subcommands(
default=env("LLMDBENCH_GATEWAY_CLASS"),
help=(
"Override the scenario's gateway.className. Supported values: "
"epponly, istio, agentgateway, gke, data-science-gateway-class. "
"none, epponly, istio, agentgateway, gke, "
"data-science-gateway-class. "
"Only takes effect on the modelservice deploy path -- ignored "
"by kustomize/standalone/fma."
),
Expand Down
5 changes: 5 additions & 0 deletions llmdbenchmark/parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ chain runs, then pops the nested copy so `config.yaml` has a single home per
section. Templates, resolvers and standup steps always read the **top-level**
keys, so the two spellings render identically -- pick one per section.

Set `gateway.className: none` to skip Gateway and router resources and expose
decode vLLM directly through a plain Service. The renderer also disables the
modelservice routing proxy so the resulting lane measures the model server
without Gateway, EPP, or Envoy overhead.

The hoist runs **before** setup overrides so the precedence stays
`defaults < scenario < treatment/CLI`: DoE experiment treatments and CLI
overrides target the **top-level** dotted path (e.g.
Expand Down
75 changes: 75 additions & 0 deletions llmdbenchmark/parser/render_plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ def _resolve_deploy_method(self, values: dict) -> dict:
# at render time so a typo doesn't silently produce a broken Gateway /
# InferencePool chart configuration.
_SUPPORTED_GATEWAY_CLASSES: tuple[str, ...] = (
"none",
"epponly",
"istio",
"agentgateway",
Expand Down Expand Up @@ -791,6 +792,71 @@ def _validate_epponly_constraints(

return errors

@staticmethod
def _normalize_direct_service_mode(values: dict) -> dict:
"""Make ``gateway.className=none`` a true direct-vLLM baseline.

The modelservice chart enables its per-pod routing proxy by default.
A plain Service pointing at that port would still put the proxy in the
request path, defeating the baseline. Disable it before templates are
rendered so the chart makes vLLM bind directly to ``servicePort``.
"""
gateway_class = (values.get("gateway") or {}).get("className", "")
modelservice_enabled = (values.get("modelservice") or {}).get("enabled", True)
if gateway_class == "none" and modelservice_enabled:
routing = values.setdefault("routing", {})
routing.setdefault("proxy", {})["enabled"] = False

# Accelerator-neutral guides may provide a custom command that
# binds decode vLLM to the proxy backend port. Direct mode has no
# proxy, so make the custom command follow the chart's normal
# proxy-disabled behavior and listen on the Service port instead.
decode_vllm = values.setdefault("decode", {}).get("vllm") or {}
custom_command = decode_vllm.get("customCommand")
if isinstance(custom_command, str):
decode_vllm["customCommand"] = custom_command.replace(
"$VLLM_METRICS_PORT",
"$VLLM_INFERENCE_PORT",
)
values["decode"]["vllm"] = decode_vllm
return values

@staticmethod
def _validate_direct_service_constraints(
values: dict,
stack_name: str,
) -> list[str]:
"""Reject configurations that require routing in direct mode."""
gateway_class = (values.get("gateway") or {}).get("className", "")
modelservice_enabled = (values.get("modelservice") or {}).get("enabled", True)
if gateway_class != "none" or not modelservice_enabled:
return []

errors: list[str] = []
http_route_mode = (values.get("httpRoute") or {}).get("mode")
if http_route_mode == "shared":
errors.append(
f"[{stack_name}] gateway.className=none cannot be used with "
"httpRoute.mode=shared (direct mode deploys no Gateway or "
"HTTPRoute)."
)

prefill = values.get("prefill") or {}
if prefill.get("enabled") and int(prefill.get("replicas", 0) or 0) > 0:
errors.append(
f"[{stack_name}] gateway.className=none cannot be used with "
"prefill replicas (direct mode bypasses P/D routing)."
)

decode = values.get("decode") or {}
decode_enabled = decode.get("enabled", int(decode.get("replicas", 0) or 0) > 0)
if not decode_enabled or int(decode.get("replicas", 0) or 0) < 1:
errors.append(
f"[{stack_name}] gateway.className=none requires at least one "
"decode replica to back the direct Service."
)
return errors

def _log_image_overrides(self, values: dict) -> None:
"""Log images that have been explicitly set (not 'auto').

Expand Down Expand Up @@ -1596,6 +1662,7 @@ def _process_stack(
merged_values, total_stacks=total_stacks
)
merged_values = self._resolve_inference_pool_host(merged_values)
merged_values = self._normalize_direct_service_mode(merged_values)
merged_values = self._normalize_router_block(merged_values)
merged_values = self._substitute_config_variables(merged_values)

Expand All @@ -1614,6 +1681,14 @@ def _process_stack(
self.logger.log_error(msg)
stack_errors.render_errors.append(msg)

direct_service_errors = self._validate_direct_service_constraints(
merged_values,
stack_name=stack_name,
)
for msg in direct_service_errors:
self.logger.log_error(msg)
stack_errors.render_errors.append(msg)

kustomize_errors = self._validate_kustomize_patches(
merged_values,
stack_name=stack_name,
Expand Down
18 changes: 18 additions & 0 deletions llmdbenchmark/run/steps/step_03_detect_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
find_fma_endpoint,
find_gateway_endpoint,
find_epponly_endpoint,
find_direct_modelservice_endpoint,
find_custom_endpoint,
find_kustomize_endpoint,
discover_hf_token_secret,
Expand Down Expand Up @@ -146,6 +147,23 @@ def execute(
namespace,
model_id_label,
)
elif gateway_class == "none":
model_id_label = plan_config.get("model_id_label", "")
direct_port = str(
self._resolve(
plan_config,
"routing.servicePort",
default="8000",
)
)
service_ip, service_name, gateway_port = (
find_direct_modelservice_endpoint(
cmd,
namespace,
model_id_label,
direct_port,
)
)
else:
service_ip, service_name, gateway_port = find_gateway_endpoint(
cmd, namespace, release
Expand Down
11 changes: 11 additions & 0 deletions llmdbenchmark/smoketests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
_rand_suffix,
compute_gateway_path_prefix,
find_custom_endpoint,
find_direct_modelservice_endpoint,
find_epponly_endpoint,
find_gateway_endpoint,
find_kustomize_endpoint,
Expand Down Expand Up @@ -144,6 +145,16 @@ def discover_endpoint(
namespace,
model_id_label,
)
elif gateway_class == "none":
direct_port = str(
_nested_get(plan_config, "routing", "servicePort") or "8000"
)
service_ip, _, gateway_port = find_direct_modelservice_endpoint(
cmd,
namespace,
model_id_label,
direct_port,
)
else:
service_ip, _, gateway_port = find_gateway_endpoint(cmd, namespace, release)

Expand Down
Loading
Loading