From bdd30eb342f4be3e49efa99d4a591f5d02044820 Mon Sep 17 00:00:00 2001 From: Andrew Mello Date: Thu, 29 Oct 2026 14:24:19 -0700 Subject: [PATCH 1/2] eat: deploy EAT release-1.1.0 on request (EAT_RELEASE), vLLM CPU v0.28.0 by digest, Qwen3Guard opt-in Intel published EAT release-1.1.0 on 2026-10-22 (0c726f5, the merge of EAT #21) and GitHub now flags release-1.0.3-hotfix as a prerelease. eat-up.sh could reach only 0d3438d. It now resolves EAT_RELEASE from a table: release-1.0.3-hotfix (default, unchanged) or release-1.1.0, each pinned to its commit; any other value is refused before the config or the cluster is read, and an EAT_WORK cloned for one release is refused for the other. The config is checked against the release before the cluster is touched, parsed as EAT's read_config_file parses it: deploy_openshell and deploy_guardrails only where the release has the role; deploy_nri_balloon_policy anything but off or no on both releases, since both turn EAT's balloons policy on when the key is absent; and on release-1.1.0, compute_platform=xpu (an unpinned GPU plugin from intel-device-plugins main beside k3d-gpu's), deploy_kata_containers=on (no /dev/kvm in a k3d node) and rack_scale_mode=on. release-1.1.0 moves every EAT vLLM CPU path to v0.28.0, by tag only. eat-up.sh mounts vllm-image-release-1.1.0.yaml (v0.28.0 at its index digest 197354e6...) for that release, and brownfield.sh merges it last through vllm-merge.py, the old inline merge moved to a file. vllm-merge.py refuses a merged tag whose version differs from the one EAT's own values name, so 1.1.0's model configs never run on v0.24.0. On release-1.0.3-hotfix the merged values are byte for byte what the inline merge wrote. release-1.1.0's deploy-agentic-stack.sh sets DEPLOY_GUARDRAILS=on at top level (line 195), which runs when brownfield.sh sources it, and reads deploy_guardrails only in main(). brownfield.sh now exports DEPLOY_GUARDRAILS from the config key before sourcing (off unless on or yes), and the shipped config sets it off. With it on, deploy-guardrails.yml alone gets remote_helm_charts_base pointing at a copy of EAT's vllm chart whose guardrail-values.yaml carries the digest, with a marker file that fails the deploy if the playbook bypasses the copy. images.lock lists the v0.28.0 digest. tests/eat_test.sh covers the release table, the per-release key matrix, the commit check offline, the bastion mounts, the guard switch and pin, model mode on both releases, vllm-merge.py, and eat-opt.py on the merged v0.28.0 values against console's optimizer for that version. --- images.lock | 8 + integrations/eat/agentic-config.cfg | 3 + integrations/eat/brownfield.sh | 105 +++- integrations/eat/eat-up.sh | 108 +++- .../eat/vllm-image-release-1.1.0.yaml | 13 + integrations/eat/vllm-merge.py | 91 +++ tests/eat_test.sh | 591 +++++++++++++++++- 7 files changed, 841 insertions(+), 78 deletions(-) create mode 100644 integrations/eat/vllm-image-release-1.1.0.yaml create mode 100755 integrations/eat/vllm-merge.py diff --git a/images.lock b/images.lock index 472f05ed..1deefcbf 100644 --- a/images.lock +++ b/images.lock @@ -283,6 +283,14 @@ ], "tag": "v0.24.0" }, + { + "digest": "sha256:197354e6475e638eb5ccca09beb078488046a82de0e204e7429bf1a804df9647", + "name": "docker.io/vllm/vllm-openai-cpu", + "sources": [ + "integrations/eat/vllm-image-release-1.1.0.yaml" + ], + "tag": "v0.28.0" + }, { "digest": "sha256:ffaba3989b9b96d95a576143c872a4110ce9800f5199b6a36eb27bdff1cbbe05", "name": "docker.io/vllm/vllm-openai-cpu", diff --git a/integrations/eat/agentic-config.cfg b/integrations/eat/agentic-config.cfg index ecfb0c75..0f12795f 100644 --- a/integrations/eat/agentic-config.cfg +++ b/integrations/eat/agentic-config.cfg @@ -14,6 +14,9 @@ deploy_pgvector=on deploy_kuberay=off deploy_agent_sandbox=on deploy_nri_balloon_policy=off +# EAT release-1.1.0 ships its Qwen3Guard guardrail on. Here it is opt-in: +# deploy_guardrails=on with EAT_RELEASE=release-1.1.0 deploys it, image by digest. +deploy_guardrails=off http_proxy= https_proxy= no_proxy= diff --git a/integrations/eat/brownfield.sh b/integrations/eat/brownfield.sh index 3889ed2a..05a893b1 100755 --- a/integrations/eat/brownfield.sh +++ b/integrations/eat/brownfield.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # Runs inside the bastion container (bastion.Dockerfile). Drives EAT's own # brownfield path, brownfield_deployment() in core/lib/brownfield/, which no -# EAT menu reaches in release-1.0.3-hotfix. Its interactive prompts get fixed -# answers; every component toggle comes from core/inventory/agentic-config.cfg. +# EAT menu reaches in release-1.0.3-hotfix or release-1.1.0. Its interactive +# prompts get fixed answers; every component toggle comes from +# core/inventory/agentic-config.cfg. # # brownfield.sh deploy the stack selected in agentic-config.cfg # brownfield.sh model deploy one model from EAT's vLLM playbook and register it in LiteLLM @@ -10,9 +11,12 @@ # EAT_DIR EAT checkout (default: /home/ubuntu/eat) # EAT_KUBECONFIG kubeconfig of the target cluster (default: /kube/config) # EAT_VLLM_VALUES values merged over EAT's vllm/xeon-values.yaml (default: /vllm-values.yaml) +# EAT_VLLM_IMAGE_VALUES the release's vLLM image overlay, merged last (default: none; +# eat-up.sh mounts vllm-image-release-1.1.0.yaml for release-1.1.0). It +# also pins the guard image when deploy_guardrails=on # EAT_VLLM_KV_GIB VLLM_CPU_KVCACHE_SPACE for every catalog model, in GiB (default: 6) # K3D_GPU_EAT_DIR integrations/eat of k3d-gpu (default: /k3d-gpu-eat): gateway-charts.py, -# eat-opt.py and the gateway values overlays +# vllm-merge.py, eat-opt.py and the gateway values overlays # K3D_GPU_CONSOLE_DIR console/ of k3d-gpu (default: /k3d-gpu-console): the model optimizer # EAT_MODEL_OPT auto | off: apply the model optimizer to the model's values (default: auto) # EAT_THINKING off | on | model: thinking default for the model (default: off) @@ -26,9 +30,11 @@ set -euo pipefail EAT_DIR="${EAT_DIR:-/home/ubuntu/eat}" EAT_KUBECONFIG="${EAT_KUBECONFIG:-/kube/config}" EAT_VLLM_VALUES="${EAT_VLLM_VALUES:-/vllm-values.yaml}" +EAT_VLLM_IMAGE_VALUES="${EAT_VLLM_IMAGE_VALUES:-}" EAT_VLLM_KV_GIB="${EAT_VLLM_KV_GIB:-6}" K3D_GPU_EAT_DIR="${K3D_GPU_EAT_DIR:-/k3d-gpu-eat}" CERT_DIR="${HOME}/certs" +CONFIG="${EAT_DIR}/core/inventory/agentic-config.cfg" mode="${1:-stack}" model_id="${2:-}" @@ -38,7 +44,36 @@ case "${mode}" in *) echo "usage: brownfield.sh [stack | model ]" >&2; exit 2 ;; esac -cluster_url=$(grep -E '^cluster_url=' "${EAT_DIR}/core/inventory/agentic-config.cfg" | cut -d= -f2-) +# config_setting : the value EAT's read_config_file gives , parsed as +# eat-up.sh parses it: split on the first '=', key and value trimmed with xargs (which +# also drops quotes), comment lines skipped, the last assignment wins. +config_setting() { + local key value v="" + while IFS='=' read -r key value || [ -n "${key}" ]; do + key=$(echo "${key}" | xargs 2>/dev/null) || continue + [ "${key}" = "$2" ] || continue + v=$(echo "${value}" | xargs 2>/dev/null) || v="${value}" + done < "$1" + echo "${v}" +} + +cluster_url=$(grep -E '^cluster_url=' "${CONFIG}" | cut -d= -f2-) + +# The content-safety guardrail (Qwen3Guard) follows the config's deploy_guardrails. +# release-1.1.0's deploy-agentic-stack.sh sets DEPLOY_GUARDRAILS="${DEPLOY_GUARDRAILS:-on}" +# at top level (line 195), which runs when it is sourced below; it reads the +# deploy_guardrails key only in main() (lines 1136-1140), which a sourced run skips, +# and fresh-install.sh (line 419) deploys the guard when the variable is on. So the +# key is applied here, whatever the environment says: on or yes deploys the guard, +# anything else (or no key) does not. release-1.0.3-hotfix never reads the variable. +case "$(config_setting "${CONFIG}" deploy_guardrails)" in + on|yes) export DEPLOY_GUARDRAILS=on ;; + *) export DEPLOY_GUARDRAILS=off ;; +esac +if [ "${mode}" = stack ] && [ "${DEPLOY_GUARDRAILS}" = on ] && [ -z "${EAT_VLLM_IMAGE_VALUES}" ]; then + echo "ERROR: deploy_guardrails=on needs EAT_VLLM_IMAGE_VALUES, the image overlay that pins the guard's vLLM by digest (eat-up.sh EAT_RELEASE=release-1.1.0)" >&2 + exit 1 +fi # EAT's one-click script makes a self-signed cert and an /etc/hosts entry; the # brownfield path expects both to exist. This is the container's /etc/hosts. @@ -77,8 +112,28 @@ if [ "${mode}" = "stack" ]; then gateway_vars="${HOME}/k3d-gpu-gateway-vars.yml" printf 'helm_charts_base_genai: %s\nhelm_charts_base_genai_trace: %s\n' \ "${charts}/genai-gateway" "${charts}/genai-gateway-trace" > "${gateway_vars}" + # The guard: deploy-guardrails.yml (release-1.1.0) installs the chart + # {{ remote_helm_charts_base }}/vllm with --values {{ remote_helm_charts_base }}/vllm/guardrail-values.yaml, + # which names vllm/vllm-openai-cpu:v0.28.0 by tag only. In that playbook, + # remote_helm_charts_base (from its vars_file inference_common.yml) serves nothing + # else, so an extra var points it at a copy of EAT's vllm chart whose + # guardrail-values.yaml carries the image overlay's digest (vllm-merge.py). + guard_vars="" + guard_marker="${HOME}/k3d-gpu-guard-pinned" + rm -f "${guard_marker}" + if [ "${DEPLOY_GUARDRAILS}" = on ]; then + guard_charts="${HOME}/k3d-gpu-guard-charts" + rm -rf "${guard_charts}" + mkdir -p "${guard_charts}" + cp -r "${CORE_DIR}/helm-charts/vllm" "${guard_charts}/vllm" + python3 "${K3D_GPU_EAT_DIR}/vllm-merge.py" --require-digest "${CORE_DIR}/helm-charts/vllm/guardrail-values.yaml" \ + "${guard_charts}/vllm/guardrail-values.yaml" "${EAT_VLLM_IMAGE_VALUES}" + guard_vars="${HOME}/k3d-gpu-guard-vars.yml" + printf 'remote_helm_charts_base: %s\n' "${guard_charts}" > "${guard_vars}" + echo "guard chart: ${guard_charts}/vllm (EAT's, guardrail-values.yaml image from ${EAT_VLLM_IMAGE_VALUES})" + fi # EAT's functions run in this shell and call ansible-playbook by name. The - # marker file records that the gateway playbook got the copies. + # marker files record that the gateway and guard playbooks got the copies. gateway_marker="${HOME}/k3d-gpu-gateway-pinned" rm -f "${gateway_marker}" ansible-playbook() { @@ -86,6 +141,10 @@ if [ "${mode}" = "stack" ]; then *"deploy-genai-gateway.yml "*) : > "${gateway_marker}" command ansible-playbook "$@" --extra-vars "@${gateway_vars}" ;; + *"deploy-guardrails.yml "*) + [ -n "${guard_vars}" ] || { echo "ERROR: deploy-guardrails.yml called with deploy_guardrails off" >&2; return 1; } + : > "${guard_marker}" + command ansible-playbook "$@" --extra-vars "@${guard_vars}" ;; *) command ansible-playbook "$@" ;; esac } @@ -96,10 +155,14 @@ if [ "${mode}" = "stack" ]; then # 1 brownfield menu: Deploy Inference Stack # yes "continue with Brownfield Deployment setup?" printf '5\n1\nyes\n' | brownfield_deployment - if grep -Eq '^deploy_genai_gateway=on' "${EAT_DIR}/core/inventory/agentic-config.cfg" && [ ! -f "${gateway_marker}" ]; then + if grep -Eq '^deploy_genai_gateway=on' "${CONFIG}" && [ ! -f "${gateway_marker}" ]; then echo "ERROR: deploy-genai-gateway.yml never ran through the pinning wrapper" >&2 exit 1 fi + if [ "${DEPLOY_GUARDRAILS}" = on ] && [ ! -f "${guard_marker}" ]; then + echo "ERROR: deploy-guardrails.yml never ran through the pinning wrapper" >&2 + exit 1 + fi exit fi @@ -116,28 +179,16 @@ setup_initial_env # leaves the shell in core/kubespray, where EAT's playbooks ru setup_bastion # EAT passes one values file to its vLLM chart (--values xeon_values_file). Give it -# EAT's own xeon-values.yaml with the k3d-gpu values merged on top, and every -# model's KV cache set to EAT_VLLM_KV_GIB, instead of editing the checkout. +# EAT's own xeon-values.yaml with the k3d-gpu values merged on top (then the +# release's image overlay, when eat-up.sh mounts one), and every model's KV cache set +# to EAT_VLLM_KV_GIB, instead of editing the checkout. vllm-merge.py stops here when +# the merged vLLM tag is not the one EAT's file names. merged="${HOME}/xeon-values.k3d-gpu.yaml" -python3 - "${HOMEDIR}/helm-charts/${xeon_values_file_path#"${REMOTEDIR}"/}" "${EAT_VLLM_VALUES}" "${EAT_VLLM_KV_GIB}" "${merged}" <<'EOF' -import sys, yaml -base_path, overlay_path, kv_gib, out = sys.argv[1:] -base = yaml.safe_load(open(base_path)) -overlay = yaml.safe_load(open(overlay_path)) or {} -def merge(dst, src): - for k, v in src.items(): - if isinstance(v, dict) and isinstance(dst.get(k), dict): - merge(dst[k], v) - else: - dst[k] = v -merge(base, overlay) -for cfg in [*(base.get("modelConfigs") or {}).values(), base.get("defaultModelConfigs")]: - cmv = (cfg or {}).get("configMapValues") or {} - if "VLLM_CPU_KVCACHE_SPACE" in cmv: - cmv["VLLM_CPU_KVCACHE_SPACE"] = kv_gib -yaml.safe_dump(base, open(out, "w"), sort_keys=False) -EOF -echo "vLLM values: ${merged} (EAT xeon-values.yaml + ${EAT_VLLM_VALUES}, KV cache ${EAT_VLLM_KV_GIB} GiB)" +overlays=("${EAT_VLLM_VALUES}") +[ -z "${EAT_VLLM_IMAGE_VALUES}" ] || overlays+=("${EAT_VLLM_IMAGE_VALUES}") +python3 "${K3D_GPU_EAT_DIR}/vllm-merge.py" --kv-gib "${EAT_VLLM_KV_GIB}" \ + "${HOMEDIR}/helm-charts/${xeon_values_file_path#"${REMOTEDIR}"/}" "${merged}" "${overlays[@]}" +echo "vLLM values: ${merged} (EAT xeon-values.yaml + ${overlays[*]}, KV cache ${EAT_VLLM_KV_GIB} GiB)" # The k3d-gpu model optimizer, as values on the model's own entry (eat-opt.py): # engine arguments EAT's entry lacks, a speculative config, the thinking default. # It prints each change against EAT stock; any failure leaves the merged values. diff --git a/integrations/eat/eat-up.sh b/integrations/eat/eat-up.sh index 057064ce..8c25fb16 100755 --- a/integrations/eat/eat-up.sh +++ b/integrations/eat/eat-up.sh @@ -5,6 +5,18 @@ # eat-up.sh deploy the stack selected in agentic-config.cfg # eat-up.sh model deploy one model from EAT's vLLM catalog and register it in LiteLLM # +# EAT_RELEASE EAT release to deploy (default: release-1.0.3-hotfix): +# release-1.0.3-hotfix commit 0d3438d; vLLM CPU v0.24.0 by digest (vllm-values.yaml) +# release-1.1.0 commit 0c726f5; vLLM CPU v0.28.0 by digest +# (vllm-image-release-1.1.0.yaml), OpenShell, and the +# Qwen3Guard guardrail on request (deploy_guardrails=on, +# its image by digest too) +# Each release is checked out at its commit only, so an EAT_WORK cloned for +# one is refused for the other. The config is checked against the release +# before the cluster is touched: EAT's NRI balloons policy is never deployed +# (k3d-gpu's stays the only one), nor, on release-1.1.0, its XPU path (a +# second, unpinned GPU plugin), Kata (no /dev/kvm in a k3d node) or +# rack-scale mode # EAT_WORK work dir for the EAT checkout and its state (default: ./eat-work) # EAT_KUBECONFIG kubeconfig of the target cluster (default: $KUBECONFIG, else ~/.kube/config) # EAT_CONFIG agentic-config.cfg to deploy with (default: the one next to this script) @@ -27,8 +39,6 @@ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" EAT_REPO="https://github.com/intel/enterprise-agent-toolkit" -EAT_TAG="release-1.0.3-hotfix" -EAT_COMMIT="0d3438d7119de44cafc5903f66665c0b623e3333" EAT_WORK="${EAT_WORK:-${PWD}/eat-work}" EAT_KUBECONFIG="${EAT_KUBECONFIG:-${KUBECONFIG:-${HOME}/.kube/config}}" EAT_CONFIG="${EAT_CONFIG:-${HERE}/agentic-config.cfg}" @@ -39,27 +49,84 @@ BASTION_IMAGE="k3d-gpu-eat-bastion:$(sha256sum "${HERE}/bastion.Dockerfile" | cu err() { echo "ERROR: $*" >&2; exit 1; } info() { echo "==> $*"; } -# openshell_setting : the deploy_openshell value EAT main would use. Parsed the -# way its read_config_file does (core/lib/system/precheck/read-config-file.sh @0ecf67a): -# split on the first '=', trim key and value with xargs (which also drops quotes), -# skip comments, and the last assignment wins because the result is sourced. -openshell_setting() { +# The releases eat-up.sh deploys: the tag's commit, the values overlay that pins the +# release's vLLM CPU image by digest, and the roles k3d-gpu checks the config against. +# release-1.0.3-hotfix needs no overlay: vllm-values.yaml pins v0.24.0, the tag that +# release names. EAT main is not offered: it is release-1.1.0 plus one picture +# (docs/pictures/Enterprise-Agent-Toolkit-Arch-Diagram.png, EAT #27). +EAT_RELEASE="${EAT_RELEASE:-release-1.0.3-hotfix}" +case "${EAT_RELEASE}" in + release-1.0.3-hotfix) + EAT_COMMIT=0d3438d7119de44cafc5903f66665c0b623e3333 + image_values="" + roles="" ;; + release-1.1.0) + EAT_COMMIT=0c726f576595971c53cc45b94e08ba969beb4a4a + image_values="${HERE}/vllm-image-release-1.1.0.yaml" + roles="openshell guardrails xpu kata rack-scale" ;; + *) err "EAT_RELEASE=${EAT_RELEASE} is not supported: use release-1.0.3-hotfix (the default) or release-1.1.0" ;; +esac +has_role() { case " ${roles} " in *" $1 "*) return 0 ;; esac; return 1; } + +# config_setting : the value EAT's read_config_file gives +# (core/lib/system/precheck/read-config-file.sh, the same at 0d3438d and 0c726f5): +# split on the first '=', key and value trimmed with xargs (which also drops quotes), +# comment lines skipped, and the last assignment wins because the result is sourced. +# EAT then reads on as yes and off as no. +config_setting() { local key value v="" while IFS='=' read -r key value || [ -n "${key}" ]; do key=$(echo "${key}" | xargs 2>/dev/null) || continue - [ "${key}" = deploy_openshell ] || continue + [ "${key}" = "$2" ] || continue v=$(echo "${value}" | xargs 2>/dev/null) || v="${value}" done < "$1" echo "${v}" } +setting() { config_setting "${EAT_CONFIG}" "$1"; } [ -f "${EAT_CONFIG}" ] || err "config ${EAT_CONFIG} not found" -# deploy_openshell arrived on EAT main after ${EAT_TAG} (PR #14); the pinned tag has no -# OpenShell role and would skip the key without a word. EAT maps on to yes and -# installs OpenShell when the value is yes (fresh-install.sh). -case "$(openshell_setting "${EAT_CONFIG}")" in - on|yes) err "deploy_openshell=on needs an EAT release newer than ${EAT_TAG}; set deploy_openshell=off" ;; +# OpenShell (EAT PR #14) and the content-safety guardrail are roles of release-1.1.0. +# release-1.0.3-hotfix would skip either key without a word. +case "$(setting deploy_openshell)" in + on|yes) has_role openshell \ + || err "deploy_openshell=on needs EAT_RELEASE=release-1.1.0: ${EAT_RELEASE} has no OpenShell role; set deploy_openshell=off" ;; +esac +# release-1.1.0 sets DEPLOY_GUARDRAILS=on at the top of deploy-agentic-stack.sh; +# brownfield.sh turns it off unless this key is on, and then deploys the guard +# from a chart copy with its image pinned by digest. +case "$(setting deploy_guardrails)" in + on|yes) has_role guardrails \ + || err "deploy_guardrails=on needs EAT_RELEASE=release-1.1.0: ${EAT_RELEASE} has no guardrail role; set deploy_guardrails=off" ;; +esac +# EAT's NRI balloons policy installs as Helm release nri-resource-policy-balloons in +# kube-system, the DaemonSet k3d-gpu's own policy already is. Both releases turn it on +# when the key is absent (parse-user-prompts.sh:55-60 at 0d3438d, :55-66 at 0c726f5, +# with compute_platform's default, cpu). +balloons=$(setting deploy_nri_balloon_policy) +case "${balloons}" in + off|no) ;; + *) err "deploy_nri_balloon_policy=${balloons:-} deploys EAT's NRI balloons policy (EAT turns it on when the key is absent), and k3d-gpu's balloons policy must remain the only one; set deploy_nri_balloon_policy=off" ;; esac +if has_role xpu; then + # deploy-intel-gpu-plugin.yml applies intel-device-plugins' gpu_plugin from main + # (intel_gpu_plugin_ref=main) beside the plugin k3d-gpu bakes and pins. + platform=$(setting compute_platform) + case "${platform}" in + x|xpu) err "compute_platform=${platform} makes ${EAT_RELEASE} apply intel-device-plugins' GPU plugin from main, unpinned, beside k3d-gpu's own; for vLLM on an Intel GPU use k3d-gpu's: K3D_GPU_AGENT_BACKEND=vllm k3d-gpu agent up" ;; + esac +fi +if has_role kata; then + case "$(setting deploy_kata_containers)" in + on|yes) err "deploy_kata_containers=on runs sandboxes in Kata microVMs, which need /dev/kvm, and k3d nodes have no /dev/kvm; set deploy_kata_containers=off" ;; + esac +fi +if has_role rack-scale; then + # rack-scale mode generates the inventory from rack-profile.yaml and pushes netplan, + # BGP and, for GPU nodes in the profile, the NVIDIA GPU operator. + case "$(setting rack_scale_mode)" in + on|yes) err "rack_scale_mode=on builds a cluster from rack-profile.yaml (netplan, BGP, the NVIDIA GPU operator); a k3d-gpu cluster is brownfield: set rack_scale_mode=off" ;; + esac +fi # The KV cache from the fit of EAT_FIT_MODEL, unless EAT_VLLM_KV_GIB is set. if [ -z "${EAT_VLLM_KV_GIB:-}" ] && [ -n "${EAT_FIT_MODEL:-}" ]; then command -v k3d-gpu >/dev/null 2>&1 || err "EAT_FIT_MODEL needs k3d-gpu on PATH (or set EAT_VLLM_KV_GIB)" @@ -82,27 +149,32 @@ for l in ei-infra-eligible ei-inference-eligible; do done if [ ! -d "${EAT_WORK}/eat/.git" ]; then - info "Cloning ${EAT_REPO} ${EAT_TAG} into ${EAT_WORK}/eat..." + info "Cloning ${EAT_REPO} ${EAT_RELEASE} into ${EAT_WORK}/eat..." mkdir -p "${EAT_WORK}" - git clone -q --depth 1 --branch "${EAT_TAG}" "${EAT_REPO}" "${EAT_WORK}/eat" + git clone -q --depth 1 --branch "${EAT_RELEASE}" "${EAT_REPO}" "${EAT_WORK}/eat" fi -[ "$(git -C "${EAT_WORK}/eat" rev-parse HEAD)" = "${EAT_COMMIT}" ] \ - || err "${EAT_WORK}/eat is not ${EAT_TAG} (${EAT_COMMIT})" +found=$(git -C "${EAT_WORK}/eat" rev-parse HEAD) +[ "${found}" = "${EAT_COMMIT}" ] \ + || err "${EAT_WORK}/eat is not ${EAT_RELEASE}: expected commit ${EAT_COMMIT}, found HEAD ${found}; use a fresh EAT_WORK for each release" cp "${EAT_CONFIG}" "${EAT_WORK}/eat/core/inventory/agentic-config.cfg" info "Building bastion image ${BASTION_IMAGE}..." docker build -q -t "${BASTION_IMAGE}" -f "${HERE}/bastion.Dockerfile" "${HERE}" >/dev/null log="${EAT_WORK}/brownfield-${1:-stack}.log" -info "Running EAT brownfield ${*:-stack} (log: ${log})..." +info "Running EAT ${EAT_RELEASE} brownfield ${*:-stack} (log: ${log})..." mkdir -p "${EAT_WORK}/hf-cache" console_mount=() [ -z "${CONSOLE_DIR}" ] || console_mount=(-v "${CONSOLE_DIR}:/k3d-gpu-console:ro") +# The release's vLLM image overlay, merged last by brownfield.sh (vllm-merge.py). +image_mount=() +[ -z "${image_values}" ] || image_mount=(-v "${image_values}:/vllm-image-values.yaml:ro" -e EAT_VLLM_IMAGE_VALUES=/vllm-image-values.yaml) docker run --rm --network host \ -v "${EAT_WORK}/eat:/home/ubuntu/eat" \ -v "${EAT_KUBECONFIG}:/kube/config:ro" \ -v "${HERE}/brownfield.sh:/brownfield.sh:ro" \ -v "${EAT_VLLM_VALUES}:/vllm-values.yaml:ro" \ + ${image_mount[@]+"${image_mount[@]}"} \ -v "${HERE}:/k3d-gpu-eat:ro" \ -v "${EAT_WORK}/hf-cache:/hf-cache" \ ${console_mount[@]+"${console_mount[@]}"} \ diff --git a/integrations/eat/vllm-image-release-1.1.0.yaml b/integrations/eat/vllm-image-release-1.1.0.yaml new file mode 100644 index 00000000..2ab5f91f --- /dev/null +++ b/integrations/eat/vllm-image-release-1.1.0.yaml @@ -0,0 +1,13 @@ +# The vLLM CPU image of EAT release-1.1.0, pinned by digest. eat-up.sh mounts this +# file for EAT_RELEASE=release-1.1.0 only. brownfield.sh merges it last: over EAT's +# vllm/xeon-values.yaml and vllm-values.yaml for catalog models, and over EAT's +# vllm/guardrail-values.yaml for the Qwen3Guard guard (deploy_guardrails=on). +# +# The tag is the one EAT names, by tag only, in core/helm-charts/vllm/xeon-values.yaml +# line 34 and guardrail-values.yaml line 30 at 0c726f576595971c53cc45b94e08ba969beb4a4a +# (release-1.1.0). The digest is the tag's multi-arch index on Docker Hub, read +# 2026-10-29 (EXPERIMENTS.md, crescent amendment E1). vllm-merge.py refuses a merge +# whose tag differs from the one in EAT's file. +image: + repository: vllm/vllm-openai-cpu + tag: "v0.28.0@sha256:197354e6475e638eb5ccca09beb078488046a82de0e204e7429bf1a804df9647" diff --git a/integrations/eat/vllm-merge.py b/integrations/eat/vllm-merge.py new file mode 100755 index 00000000..dfe0c5e4 --- /dev/null +++ b/integrations/eat/vllm-merge.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""vllm-merge.py: k3d-gpu's values merged over one of EAT's vLLM chart values files. + + vllm-merge.py [--kv-gib GIB] [--require-digest] BASE OUT [OVERLAY...] + +BASE is EAT's file (vllm/xeon-values.yaml, or vllm/guardrail-values.yaml for the +guard). Each OVERLAY is merged over it in order, a mapping into a mapping and +anything else replaced. brownfield.sh passes vllm-values.yaml, then the release's +image overlay when eat-up.sh mounts one (EAT_VLLM_IMAGE_VALUES). With --kv-gib, +every modelConfigs entry and defaultModelConfigs that sets VLLM_CPU_KVCACHE_SPACE +gets GIB instead. OUT is written only when the checks pass; BASE is never written. + +Checks, each a non-zero exit with one line on stderr: + - the merged image.tag, up to any '@', is the tag BASE names. EAT's model configs + are written for the vLLM its release ships, so an overlay may add a digest to + that tag but not swap the version (release-1.1.0's configs on v0.24.0 is the + stall in EXPERIMENTS.md crescent amendment E1) + - with --require-digest, the merged tag is @sha256:<64 hex> + +Needs python3 and PyYAML (the bastion has both). +""" + +import argparse +import os +import re +import sys + +import yaml + +PINNED = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$") + + +def die(msg): + sys.stderr.write("vllm-merge: %s\n" % msg) + sys.exit(1) + + +def load(path): + with open(path, encoding="utf-8") as f: + return yaml.safe_load(f) or {} + + +def merge(dst, src): + for k, v in src.items(): + if isinstance(v, dict) and isinstance(dst.get(k), dict): + merge(dst[k], v) + else: + dst[k] = v + + +def image_tag(values): + tag = (values.get("image") or {}).get("tag") + return None if tag is None else str(tag) + + +def main(argv): + ap = argparse.ArgumentParser(prog="vllm-merge.py") + ap.add_argument("--kv-gib", help="VLLM_CPU_KVCACHE_SPACE for every entry that sets it") + ap.add_argument("--require-digest", action="store_true", help="the merged image tag must carry @sha256:") + ap.add_argument("base") + ap.add_argument("out") + ap.add_argument("overlays", nargs="*") + a = ap.parse_args(argv[1:]) + + values = load(a.base) + eat_tag = image_tag(values) + if eat_tag is None: + die("%s names no image.tag to check the overlays against" % a.base) + for path in a.overlays: + merge(values, load(path)) + tag = image_tag(values) or "" + if tag.split("@")[0] != eat_tag.split("@")[0]: + die("the merged image tag is %s, but EAT's %s names %s: its model configs are for %s. " + "This EAT release needs its own image overlay (eat-up.sh EAT_RELEASE)" + % (tag, a.base, eat_tag, eat_tag.split("@")[0])) + if a.require_digest and not PINNED.match(tag): + die("image.tag %s is not @sha256:" % tag) + if a.kv_gib is not None: + for cfg in [*(values.get("modelConfigs") or {}).values(), values.get("defaultModelConfigs")]: + cmv = (cfg or {}).get("configMapValues") or {} + if "VLLM_CPU_KVCACHE_SPACE" in cmv: + cmv["VLLM_CPU_KVCACHE_SPACE"] = a.kv_gib + + with open(a.out + ".tmp", "w", encoding="utf-8") as f: + yaml.safe_dump(values, f, sort_keys=False) + os.replace(a.out + ".tmp", a.out) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tests/eat_test.sh b/tests/eat_test.sh index 177b6b33..95a44a29 100755 --- a/tests/eat_test.sh +++ b/tests/eat_test.sh @@ -9,12 +9,22 @@ # both for a router that shlex.splits the command (agent-sandbox v0.5.0 # python-runtime-sandbox) and for one that runs it under /bin/sh -c # - the check fails when the sandbox prints anything but 385 -# 3. eat-up.sh refuses deploy_openshell=on, which the pinned EAT tag cannot deploy. +# 3. eat-up.sh resolves EAT_RELEASE (release-1.0.3-hotfix by default, or +# release-1.1.0) and checks the config against that release before it touches +# the cluster: OpenShell and the guardrail only where the release has the role, +# never EAT's NRI balloons policy, and on release-1.1.0 no XPU path, Kata or +# rack-scale mode. Then the release's commit, offline, and what the bastion gets. # 5. The GenAI Gateway installs from copies of its charts with exact dependency # versions and every image name:tag@sha256 (gateway-charts.py, brownfield.sh). +# brownfield.sh turns EAT's guardrail on only from deploy_guardrails, and then +# from a chart copy with the guard image pinned by digest; in model mode it +# refuses EAT values whose vLLM tag the overlays do not match. # 6. eat-opt.py puts the model optimizer's choices on EAT's own modelConfigs entry, -# additive only, and prints each change against EAT stock. -# Needs bash, jq, python3 (with PyYAML). +# additive only, and prints each change against EAT stock, at the chart's vLLM +# version (v0.24.0 and, on release-1.1.0's values, v0.28.0). +# 7. vllm-merge.py merges the k3d-gpu overlays over EAT's values and refuses a +# merged vLLM tag other than the one EAT's own file names. +# Needs bash, git, jq, python3 (with PyYAML). set -euo pipefail @@ -31,7 +41,9 @@ fail() { FAILS=$((FAILS + 1)); echo "FAIL $1"; } # 1. File modes in the index, which is what a fresh checkout gets. while read -r mode _ _ path; do if [ "${mode}" = 100755 ]; then ok "executable in git: ${path}"; else fail "not executable in git (${mode}): ${path}"; fi -done < <(git -C "${ROOT}" ls-files -s -- 'integrations/eat/*.sh' 'integrations/eat/**/*.sh' 'integrations/eat/**/*.py') +done < <(git -C "${ROOT}" ls-files -s -- 'integrations/eat/*.sh' 'integrations/eat/*.py' 'integrations/eat/**/*.sh' 'integrations/eat/**/*.py') +git -C "${ROOT}" ls-files --error-unmatch -- integrations/eat/vllm-merge.py >/dev/null 2>&1 \ + && ok "tracked in git: integrations/eat/vllm-merge.py" || fail "not tracked in git: integrations/eat/vllm-merge.py" # 2. eat-check.sh against stubs. BIN="${WORK}/bin" @@ -237,29 +249,37 @@ else fi kill "${ESTUB_PID}" 2>/dev/null || true -# 3. eat-up.sh refuses deploy_openshell=on before it touches the cluster: the pinned -# release-1.0.3-hotfix has no OpenShell role. No kubectl stub: it must stop first. -# The spellings follow EAT main's read_config_file: trimmed key and value, quotes -# dropped, on mapped to yes, last assignment wins. -REFUSED='deploy_openshell=on needs an EAT release newer than release-1.0.3-hotfix' +# 3. eat-up.sh checks the config against the EAT release before it touches the +# cluster. No kubectl stub: a config that passes stops at the missing kubeconfig. +# The spellings follow EAT's read_config_file (the same at 0d3438d and 0c726f5): +# trimmed key and value, quotes dropped, on mapped to yes, last assignment wins. +# With EAT_RELEASE unset, release-1.0.3-hotfix has no OpenShell role. +REFUSED='deploy_openshell=on needs EAT_RELEASE=release-1.1.0' PASSED='kubeconfig /nonexistent not found' N=0 -# openshell_case -openshell_case() { - local name=$1 want=$2 out rc msg cfg - shift 2 +BASE_CFG="${EAT}/agentic-config.cfg" +# cfg_case +# +cfg_case() { + local release=$1 name=$2 want=$3 msg=$4 out rc cfg envs=() + shift 4 N=$((N + 1)) - cfg="${WORK}/openshell-${N}.cfg" - cp "${EAT}/agentic-config.cfg" "${cfg}" + cfg="${WORK}/cfg-${N}.cfg" + cp "${BASE_CFG}" "${cfg}" [ "$#" -eq 0 ] || printf '%s\n' "$@" >> "${cfg}" - out=$(EAT_CONFIG="${cfg}" EAT_KUBECONFIG=/nonexistent "${EAT}/eat-up.sh" 2>&1) && rc=0 || rc=$? - if [ "${want}" = refuse ]; then msg=${REFUSED}; else msg=${PASSED}; fi - if [ "${rc}" -ne 0 ] && grep -qF "${msg}" <<< "${out}"; then - ok "eat-up ${want}: ${name}" + [ -z "${release}" ] || envs=(EAT_RELEASE="${release}") + out=$(env -u EAT_RELEASE ${envs[@]+"${envs[@]}"} EAT_CONFIG="${cfg}" EAT_KUBECONFIG=/nonexistent \ + "${EAT}/eat-up.sh" 2>&1) && rc=0 || rc=$? + [ "${want}" = refuse ] || msg=${PASSED} + if [ "${rc}" -ne 0 ] && grep -qF -- "${msg}" <<< "${out}" \ + && { [ "${want}" = pass ] || ! grep -qF "${PASSED}" <<< "${out}"; }; then + ok "eat-up ${release:-default} ${want}: ${name}" else - fail "eat-up ${want}: ${name} (rc=${rc}): ${out}" + fail "eat-up ${release:-default} ${want}: ${name} (rc=${rc}): ${out}" fi } +# openshell_case : the default release. +openshell_case() { local name=$1 want=$2; shift 2; cfg_case "" "${name}" "${want}" "${REFUSED}" "$@"; } openshell_case "shipped config" pass openshell_case "deploy_openshell=on" refuse 'deploy_openshell=on' openshell_case "spaces around =" refuse 'deploy_openshell = on' @@ -307,6 +327,172 @@ openshell_case "commented out" pass '# deploy_openshell=on' openshell_case "ON is not on to EAT" pass 'deploy_openshell=ON' openshell_case "other key mentioning it" pass 'note=deploy_openshell=on' +# 3a. EAT_RELEASE: two releases, each with its own checks. release-1.1.0 has the +# OpenShell role, so the same spellings reach the kubeconfig check there. +cfg_case release-1.0.3-hotfix "deploy_openshell=on, release named" refuse "${REFUSED}" 'deploy_openshell=on' +cfg_case release-1.1.0 "shipped config" pass "" +cfg_case release-1.1.0 "deploy_openshell=on" pass "" 'deploy_openshell=on' +cfg_case release-1.1.0 "deploy_openshell = \"yes\"" pass "" 'deploy_openshell = "yes"' +for r in release-1.2.0 main release-1.0.3 ''; do + out=$(EAT_RELEASE="${r}" EAT_CONFIG=/nonexistent EAT_KUBECONFIG=/nonexistent "${EAT}/eat-up.sh" 2>&1) && rc=0 || rc=$? + if [ -z "${r}" ]; then + # set but empty is unset: the default release, which then needs the config + if [ "${rc}" -ne 0 ] && grep -qF 'config /nonexistent not found' <<< "${out}"; then ok "eat-up: EAT_RELEASE='' is the default release" + else fail "eat-up: EAT_RELEASE='' (rc=${rc}): ${out}"; fi + continue + fi + if [ "${rc}" -ne 0 ] && grep -qF "EAT_RELEASE=${r} is not supported" <<< "${out}" \ + && grep -qF 'release-1.0.3-hotfix' <<< "${out}" && grep -qF 'release-1.1.0' <<< "${out}" \ + && ! grep -qF 'not found' <<< "${out}"; then + ok "eat-up: EAT_RELEASE=${r} refused, the supported releases listed, before the config and kubeconfig" + else + fail "eat-up: EAT_RELEASE=${r} (rc=${rc}): ${out}" + fi +done + +# deploy_nri_balloon_policy, both releases: anything but off or no installs EAT's +# NRI balloons policy, a second one beside k3d-gpu's. Both releases turn it on when +# the key is absent (parse-user-prompts.sh:55-60 at 0d3438d, :55-66 at 0c726f5, where +# compute_platform defaults to cpu). +BALLOONS="k3d-gpu's balloons policy must remain the only one" +grep -v '^deploy_nri_balloon_policy=' "${EAT}/agentic-config.cfg" > "${WORK}/no-balloons.cfg" +for rel in release-1.0.3-hotfix release-1.1.0; do + BASE_CFG="${WORK}/no-balloons.cfg" + cfg_case "${rel}" "deploy_nri_balloon_policy absent" refuse "${BALLOONS}" + cfg_case "${rel}" "deploy_nri_balloon_policy= (empty)" refuse "${BALLOONS}" 'deploy_nri_balloon_policy=' + cfg_case "${rel}" "deploy_nri_balloon_policy only commented out" refuse "${BALLOONS}" '# deploy_nri_balloon_policy=off' + cfg_case "${rel}" "deploy_nri_balloon_policy=on" refuse "${BALLOONS}" 'deploy_nri_balloon_policy=on' + cfg_case "${rel}" "deploy_nri_balloon_policy = \"yes\"" refuse "${BALLOONS}" 'deploy_nri_balloon_policy = "yes"' + cfg_case "${rel}" "deploy_nri_balloon_policy=yes, indented" refuse "${BALLOONS}" ' deploy_nri_balloon_policy=yes' + cfg_case "${rel}" "deploy_nri_balloon_policy off then on, last wins" refuse "${BALLOONS}" \ + 'deploy_nri_balloon_policy=off' 'deploy_nri_balloon_policy=on' + cfg_case "${rel}" "deploy_nri_balloon_policy=off" pass "" 'deploy_nri_balloon_policy=off' + cfg_case "${rel}" "deploy_nri_balloon_policy=no" pass "" 'deploy_nri_balloon_policy=no' + cfg_case "${rel}" "deploy_nri_balloon_policy='off'" pass "" "deploy_nri_balloon_policy='off'" + cfg_case "${rel}" "deploy_nri_balloon_policy on then no, last wins" pass "" \ + 'deploy_nri_balloon_policy=on' 'deploy_nri_balloon_policy=no' + BASE_CFG="${EAT}/agentic-config.cfg" +done + +# release-1.1.0 only: its XPU path would add a second GPU plugin, Kata needs /dev/kvm, +# and rack-scale mode builds its own cluster. +XPU='K3D_GPU_AGENT_BACKEND=vllm' +cfg_case release-1.1.0 "compute_platform=xpu" refuse "${XPU}" 'compute_platform=xpu' +cfg_case release-1.1.0 "compute_platform = x" refuse "${XPU}" 'compute_platform = x' +cfg_case release-1.1.0 "compute_platform=\"xpu\"" refuse "${XPU}" 'compute_platform="xpu"' +cfg_case release-1.1.0 "compute_platform=cpu" pass "" 'compute_platform=cpu' +cfg_case release-1.1.0 "compute_platform xpu then c, last wins" pass "" 'compute_platform=xpu' 'compute_platform=c' +KATA='k3d nodes have no /dev/kvm' +cfg_case release-1.1.0 "deploy_kata_containers=on" refuse "${KATA}" 'deploy_kata_containers=on' +cfg_case release-1.1.0 "deploy_kata_containers = yes" refuse "${KATA}" 'deploy_kata_containers = yes' +cfg_case release-1.1.0 "deploy_kata_containers=off" pass "" 'deploy_kata_containers=off' +RACK='a k3d-gpu cluster is brownfield' +cfg_case release-1.1.0 "rack_scale_mode=on" refuse "${RACK}" 'rack_scale_mode=on' +cfg_case release-1.1.0 "rack_scale_mode=off" pass "" 'rack_scale_mode=off' + +# deploy_guardrails: release-1.0.3-hotfix has no guardrail role; release-1.1.0 +# deploys Qwen3Guard on request, with its image pinned by digest (section 5). +GUARD='deploy_guardrails=on needs EAT_RELEASE=release-1.1.0' +cfg_case "" "deploy_guardrails=on" refuse "${GUARD}" 'deploy_guardrails=on' +cfg_case "" "deploy_guardrails = 'yes'" refuse "${GUARD}" "deploy_guardrails = 'yes'" +cfg_case "" "deploy_guardrails off then on, last wins" refuse "${GUARD}" 'deploy_guardrails=off' 'deploy_guardrails=on' +cfg_case "" "deploy_guardrails on then off, last wins" pass "" 'deploy_guardrails=on' 'deploy_guardrails=off' +cfg_case release-1.1.0 "deploy_guardrails=on" pass "" 'deploy_guardrails=on' +cfg_case release-1.1.0 "deploy_guardrails = \"yes\"" pass "" 'deploy_guardrails = "yes"' + +# 3c. The release's commit, offline. kubectl answers the two node-label queries. +# docker logs its calls; with STUB_NO_DOCKER set it fails, and the log must stay +# empty: eat-up.sh stops before `docker build`. +SB="${WORK}/sbin" +mkdir -p "${SB}" +cat > "${SB}/kubectl" <<'EOF' +#!/usr/bin/env bash +case "$*" in + "get nodes -l ei-infra-eligible=true -o name"|"get nodes -l ei-inference-eligible=true -o name") echo node/k3d-eat-server-0 ;; + *) echo "kubectl stub: unexpected: $*" >&2; exit 1 ;; +esac +EOF +cat > "${SB}/docker" <<'EOF' +#!/usr/bin/env bash +echo "docker $*" >> "${STUB_LOG}" +[ -z "${STUB_NO_DOCKER:-}" ] || { echo "docker stub: must not be called: $*" >&2; exit 99; } +EOF +chmod +x "${SB}/kubectl" "${SB}/docker" +C103=0d3438d7119de44cafc5903f66665c0b623e3333 +C110=0c726f576595971c53cc45b94e08ba969beb4a4a +# A real local repository whose HEAD is neither release. +CW="${WORK}/cw" +mkdir -p "${CW}/eat/core/inventory" +git -C "${CW}/eat" init -q +git -C "${CW}/eat" -c user.name=t -c user.email=t@example.com -c commit.gpgsign=false -c core.hooksPath=/dev/null \ + commit -q --allow-empty -m other +OTHER=$(git -C "${CW}/eat" rev-parse HEAD) +for rel in release-1.0.3-hotfix release-1.1.0; do + if [ "${rel}" = release-1.1.0 ]; then want=${C110}; else want=${C103}; fi + : > "${WORK}/dlog" + out=$(env PATH="${SB}:${PATH}" STUB_LOG="${WORK}/dlog" STUB_NO_DOCKER=1 EAT_RELEASE="${rel}" EAT_WORK="${CW}" \ + EAT_KUBECONFIG="${WORK}/kubeconfig" "${EAT}/eat-up.sh" 2>&1) && rc=0 || rc=$? + if [ "${rc}" -ne 0 ] && grep -qF "is not ${rel}" <<< "${out}" && grep -qF "expected commit ${want}" <<< "${out}" \ + && grep -qF "found HEAD ${OTHER}" <<< "${out}" && grep -qF 'fresh EAT_WORK' <<< "${out}" && [ ! -s "${WORK}/dlog" ]; then + ok "eat-up ${rel}: a checkout at another commit is refused, naming the release and both commits, before docker" + else + fail "eat-up ${rel}: commit check (rc=${rc}): ${out} $(cat "${WORK}/dlog")" + fi +done +# What the bastion gets, with a git stub reporting HEAD: the default release mounts no +# image overlay; release-1.1.0 mounts vllm-image-release-1.1.0.yaml and names it in +# EAT_VLLM_IMAGE_VALUES. An EAT_WORK cloned for one release is refused for the other. +GB="${WORK}/gbin" +BW="${WORK}/bw" +mkdir -p "${GB}" "${BW}/eat/.git" "${BW}/eat/core/inventory" +cat > "${GB}/git" <<'EOF' +#!/usr/bin/env bash +case "$*" in + *" rev-parse HEAD") echo "${STUB_HEAD}" ;; + *) echo "git stub: unexpected: $*" >&2; exit 1 ;; +esac +EOF +chmod +x "${GB}/git" +# bastion : eat-up.sh's output; the docker calls in ${WORK}/dlog. +bastion() { + local envs=() + [ -z "$1" ] || envs=(EAT_RELEASE="$1") + : > "${WORK}/dlog" + env -u EAT_RELEASE -u EAT_VLLM_IMAGE_VALUES PATH="${GB}:${SB}:${PATH}" STUB_LOG="${WORK}/dlog" STUB_HEAD="$2" \ + EAT_WORK="${BW}" EAT_KUBECONFIG="${WORK}/kubeconfig" ${envs[@]+"${envs[@]}"} "${EAT}/eat-up.sh" 2>&1 +} +out=$(bastion "" "${C103}") && rc=0 || rc=$? +run=$(grep '^docker run ' "${WORK}/dlog" || true) +if [ "${rc}" -eq 0 ] && grep -qF -- "-v ${EAT}/vllm-values.yaml:/vllm-values.yaml:ro" <<< "${run}" \ + && grep -qF -- "-v ${BW}/eat:/home/ubuntu/eat" <<< "${run}" && ! grep -qF 'vllm-image' <<< "${run}" \ + && ! grep -qF 'EAT_VLLM_IMAGE_VALUES' <<< "${run}" && cmp -s "${EAT}/agentic-config.cfg" "${BW}/eat/core/inventory/agentic-config.cfg"; then + ok "eat-up default: release-1.0.3-hotfix at ${C103:0:7} reaches the bastion with vllm-values.yaml alone" +else + fail "eat-up default bastion (rc=${rc}): ${out} | ${run}" +fi +out=$(bastion release-1.1.0 "${C110}") && rc=0 || rc=$? +run=$(grep '^docker run ' "${WORK}/dlog" || true) +if [ "${rc}" -eq 0 ] && grep -qF -- "-v ${EAT}/vllm-values.yaml:/vllm-values.yaml:ro" <<< "${run}" \ + && grep -qF -- "-v ${EAT}/vllm-image-release-1.1.0.yaml:/vllm-image-values.yaml:ro" <<< "${run}" \ + && grep -qF -- "-e EAT_VLLM_IMAGE_VALUES=/vllm-image-values.yaml" <<< "${run}"; then + ok "eat-up release-1.1.0: ${C110:0:7} reaches the bastion with the v0.28.0 image overlay mounted" +else + fail "eat-up release-1.1.0 bastion (rc=${rc}): ${out} | ${run}" +fi +# cross +cross() { + local out rc + out=$(bastion "$1" "$3") && rc=0 || rc=$? + if [ "${rc}" -ne 0 ] && grep -qF "is not ${1:-release-1.0.3-hotfix}" <<< "${out}" && grep -qF "expected commit $2" <<< "${out}" \ + && grep -qF "found HEAD $3" <<< "${out}" && [ ! -s "${WORK}/dlog" ]; then + ok "eat-up ${1:-default}: an EAT_WORK cloned for the other release (${3:0:7}) is refused" + else + fail "eat-up ${1:-default}: cross-release EAT_WORK (rc=${rc}): ${out}" + fi +} +cross release-1.1.0 "${C110}" "${C103}" +cross "" "${C103}" "${C110}" + # 5. The GenAI Gateway path installs pinned charts and images. EAT's # deploy-genai-gateway.yml syncs helm_charts_base_genai{,_trace} to the node, runs # `helm dependency update` on genai-gateway (Chart.yaml ranges postgresql >=13.3.0, @@ -479,23 +665,109 @@ gw_refuses "a Langfuse values file without a key the overlay pins" 'zookeeper' \ gw_refuses "an image repository the overlay does not expect" 'bitnami/redis' \ "sed -i 's#bitnamilegacy/redis#bitnami/redis#' genai-gateway/values.yaml" -# brownfield.sh stack mode against a stub EAT: the gateway playbook, and only it, -# gets the chart copies as extra vars; the menu answers are unchanged. +# EAT's vllm/xeon-values.yaml at 0d3438d (release-1.0.3-hotfix) and 0c726f5 +# (release-1.1.0), cut to the keys vllm-merge.py, the chart and eat-opt.py read: the +# image, the balloon annotation and resources, one catalog entry and the default +# entry, each with VLLM_CPU_KVCACHE_SPACE. +VM="${WORK}/vm" +mkdir -p "${VM}" +for rel in 103 110; do + if [ "${rel}" = 103 ]; then tag=v0.24.0 tp="'{{ .Values.tensor_parallel_size }}'"; else tag=v0.28.0 tp=1; fi + cat > "${VM}/xeon-${rel}.yaml" <: its image.tag +tag_of() { python3 -c 'import sys, yaml; print(yaml.safe_load(open(sys.argv[1]))["image"]["tag"])' "$1" 2>/dev/null; } + +# brownfield.sh against a stub EAT. Stack mode: the gateway playbook, and only it, +# gets the chart copies as extra vars; the menu answers are unchanged. The stub's +# deploy-agentic-stack.sh sets DEPLOY_GUARDRAILS at top level as release-1.1.0's does +# (line 195, run when brownfield.sh sources it) and deploys the guard through +# deploy-guardrails.yml when it is on, as fresh-install.sh:419 does. Model mode: the +# calls brownfield.sh makes, each a stub, with EAT's xeon-values.yaml in the checkout. BF="${WORK}/bf" mkdir -p "${BF}/bin" "${BF}/home" -cp -r "${FIX}" "${BF}/eat" -echo 'cluster_url=api.example.com' > "${BF}/eat/core/inventory/agentic-config.cfg" -cat > "${BF}/eat/deploy-agentic-stack.sh" <<'EOF' -_source_core_libs() { CORE_DIR="${EAT_DIR}/core"; } +# EAT 0c726f5's core/helm-charts/vllm, cut to what the guard's helm call reads. +VC="${WORK}/vllm-chart" +mkdir -p "${VC}/templates" +printf 'apiVersion: v2\nname: vllm\nversion: 0.1.0\n' > "${VC}/Chart.yaml" +printf 'image:\n repository: vllm/vllm-openai-cpu\n' > "${VC}/values.yaml" +cat > "${VC}/templates/deployment.yaml" <<'EOF' + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" +EOF +cat > "${VC}/guardrail-values.yaml" <<'EOF' +fullnameOverride: vllm-guard +LLM_MODEL_ID: "Qwen/Qwen3Guard-Gen-0.6B" +cpu: 2 +memory: 16Gi +image: + repository: vllm/vllm-openai-cpu + tag: "v0.28.0" + pullPolicy: IfNotPresent +defaultModelConfigs: + configMapValues: + VLLM_CPU_KVCACHE_SPACE: "4" + extraCmdArgs: ["--block-size", "128", "--dtype", "bfloat16", "--max-num-seqs", "8"] +modelConfigs: + "Qwen/Qwen3Guard-Gen-0.6B": + optimal_cpu_cores: 8 +EOF +cp "${VM}/xeon-110.yaml" "${VC}/xeon-values.yaml" +# bf_eat : a fresh stub EAT checkout with that agentic-config.cfg. +bf_eat() { + rm -rf "${BF}/eat" + cp -r "${FIX}" "${BF}/eat" + cp -r "${VC}" "${BF}/eat/core/helm-charts/vllm" + printf '%s\n' "$@" > "${BF}/eat/core/inventory/agentic-config.cfg" + cat > "${BF}/eat/deploy-agentic-stack.sh" <<'EOF' +DEPLOY_GUARDRAILS="${DEPLOY_GUARDRAILS:-on}" +_source_core_libs() { CORE_DIR="${EAT_DIR}/core"; HOMEDIR="${CORE_DIR}"; } brownfield_deployment() { local a b c read -r a; read -r b; read -r c echo "answers ${a} ${b} ${c}" >> "${STUB_LOG}" + echo "DEPLOY_GUARDRAILS=${DEPLOY_GUARDRAILS}" >> "${STUB_LOG}" cd "${CORE_DIR}" ansible-playbook -i hosts playbooks/deploy-ingress-controller.yml --extra-vars "secret_name=api.example.com" ansible-playbook -i hosts playbooks/deploy-genai-gateway.yml --extra-vars "secret_name=api.example.com" --vault-password-file v + if [[ "${DEPLOY_GUARDRAILS:-off}" == "on" ]]; then + ansible-playbook -i hosts playbooks/deploy-guardrails.yml --extra-vars "hugging_face_token= guardrail_model=Qwen/Qwen3Guard-Gen-0.6B" --vault-password-file v + fi } +manage_kubeconfig() { :; } +read_config_file() { :; } +prompt_for_input() { :; } +setup_initial_env() { REMOTEDIR=/tmp/helm-charts; xeon_values_file_path="${REMOTEDIR}/vllm/xeon-values.yaml"; brownfield_deployment_host_file=bastion-hosts.yaml; } +setup_bastion() { :; } +deploy_inference_llm_models_playbook() { echo "models ${model_name_list} values ${xeon_values_file_path} inventory ${INVENTORY_PATH}" >> "${STUB_LOG}"; } EOF +} cat > "${BF}/bin/sudo" <<'EOF' #!/usr/bin/env bash exit 0 @@ -512,9 +784,18 @@ for a in "$@"; do done EOF chmod +x "${BF}/bin/"* -: > "${BF}/log" -out=$(env PATH="${BF}/bin:${PATH}" HOME="${BF}/home" STUB_LOG="${BF}/log" EAT_DIR="${BF}/eat" \ - EAT_KUBECONFIG="${WORK}/kubeconfig" K3D_GPU_EAT_DIR="${EAT}" bash "${EAT}/brownfield.sh" 2>&1) && rc=0 || rc=$? +# bf_run [VAR=value...] [-- brownfield.sh args...]: brownfield.sh's output; its calls in ${BF}/log. +bf_run() { + local envs=() + while [ $# -gt 0 ] && [ "$1" != -- ]; do envs+=("$1"); shift; done + [ $# -eq 0 ] || shift + : > "${BF}/log" + env -u DEPLOY_GUARDRAILS -u EAT_VLLM_IMAGE_VALUES PATH="${BF}/bin:${PATH}" HOME="${BF}/home" STUB_LOG="${BF}/log" \ + EAT_DIR="${BF}/eat" EAT_KUBECONFIG="${WORK}/kubeconfig" K3D_GPU_EAT_DIR="${EAT}" ${envs[@]+"${envs[@]}"} \ + bash "${EAT}/brownfield.sh" "$@" 2>&1 +} +bf_eat cluster_url=api.example.com +out=$(bf_run) && rc=0 || rc=$? log=$(cat "${BF}/log") if [ "${rc}" -eq 0 ] && grep -qx 'answers 5 1 yes' <<< "${log}"; then ok "brownfield.sh stack: runs with the same menu answers" else fail "brownfield.sh stack rc=${rc}: ${out} ${log}"; fi @@ -531,15 +812,111 @@ ingress_call=$(grep '^ansible-playbook .*deploy-ingress-controller.yml' <<< "${l if [ "${ingress_call}" = 'ansible-playbook -i hosts playbooks/deploy-ingress-controller.yml --extra-vars secret_name=api.example.com' ]; then ok "brownfield.sh stack: other playbooks run as EAT calls them" else fail "brownfield.sh stack: ingress call: ${ingress_call}"; fi +if grep -qx 'DEPLOY_GUARDRAILS=off' <<< "${log}" && ! grep -q 'deploy-guardrails.yml' <<< "${log}"; then + ok "brownfield.sh stack: no deploy_guardrails key, no guard (release-1.1.0 alone would default it on)" +else fail "brownfield.sh stack: guard without deploy_guardrails: ${log}"; fi + +# The guardrail switch comes from the config, never from the environment. +# guard_case [bf_run VAR=value...] -- +guard_case() { + local name=$1 want=$2 envs=() out rc log + shift 2 + while [ "$1" != -- ]; do envs+=("$1"); shift; done + shift + bf_eat cluster_url=api.example.com "$@" + out=$(bf_run ${envs[@]+"${envs[@]}"}) && rc=0 || rc=$? + log=$(cat "${BF}/log") + if [ "${rc}" -eq 0 ] && grep -qx "DEPLOY_GUARDRAILS=${want}" <<< "${log}"; then ok "brownfield.sh stack: ${name}: guard ${want}" + else fail "brownfield.sh stack: ${name} (rc=${rc}, want ${want}): ${out} ${log}"; fi +} +mapfile -t SHIPPED < <(grep -v '^cluster_url=' "${EAT}/agentic-config.cfg") +guard_case "shipped agentic-config.cfg" off -- "${SHIPPED[@]}" +guard_case "deploy_guardrails=off with DEPLOY_GUARDRAILS=on in the environment" off DEPLOY_GUARDRAILS=on -- deploy_guardrails=off +guard_case "deploy_guardrails=ON is not on to EAT" off -- deploy_guardrails=ON +guard_case "deploy_guardrails=on" on EAT_VLLM_IMAGE_VALUES="${IMG110}" -- deploy_guardrails=on +guard_case "deploy_guardrails = \"yes\"" on EAT_VLLM_IMAGE_VALUES="${IMG110}" -- 'deploy_guardrails = "yes"' +guard_case "deploy_guardrails on then off, last wins" off -- deploy_guardrails=on deploy_guardrails=off + +# deploy_guardrails=on: deploy-guardrails.yml, and only it, gets a copy of EAT's vllm +# chart whose guardrail-values.yaml names the guard image by digest. +bf_eat cluster_url=api.example.com deploy_guardrails=on +out=$(bf_run EAT_VLLM_IMAGE_VALUES="${IMG110}") && rc=0 || rc=$? +log=$(cat "${BF}/log") +guard_call=$(grep '^ansible-playbook .*deploy-guardrails.yml' <<< "${log}" || true) +GC="${BF}/home/k3d-gpu-guard-charts" +if [ "${rc}" -eq 0 ] && grep -qF -- "--extra-vars @${BF}/home/k3d-gpu-guard-vars.yml" <<< "${guard_call}" \ + && grep -qx "remote_helm_charts_base: ${GC}" <<< "${log}" \ + && [ "$(tag_of "${GC}/vllm/guardrail-values.yaml")" = "${PIN110}" ] \ + && cmp -s "${VC}/Chart.yaml" "${GC}/vllm/Chart.yaml" && cmp -s "${VC}/templates/deployment.yaml" "${GC}/vllm/templates/deployment.yaml" \ + && [ "$(tag_of "${BF}/eat/core/helm-charts/vllm/guardrail-values.yaml")" = v0.28.0 ]; then + ok "brownfield.sh stack: deploy_guardrails=on installs the guard from a chart copy at ${PIN110:0:16}..., EAT's checkout untouched" +else + fail "brownfield.sh stack: guard call (rc=${rc}): ${out} | ${guard_call}"; sed 's/^/ | /' <<< "${log}" +fi +if python3 - "${GC}/vllm/guardrail-values.yaml" <<'PY' +import sys, yaml +v = yaml.safe_load(open(sys.argv[1])) +assert v["image"] == {"repository": "vllm/vllm-openai-cpu", "tag": v["image"]["tag"], "pullPolicy": "IfNotPresent"}, v["image"] +assert v["cpu"] == 2 and v["memory"] == "16Gi" and v["fullnameOverride"] == "vllm-guard", v +assert v["defaultModelConfigs"]["configMapValues"]["VLLM_CPU_KVCACHE_SPACE"] == "4", v +assert v["modelConfigs"]["Qwen/Qwen3Guard-Gen-0.6B"]["optimal_cpu_cores"] == 8, v +PY +then ok "brownfield.sh stack: the guard keeps EAT's values but the image (cpu 2, 16Gi, KV 4 GiB)" +else fail "brownfield.sh stack: the guard's values changed beyond the image"; fi +calls=$(grep '^ansible-playbook ' <<< "${log}") +if [ "$(grep -c -- '--extra-vars @' <<< "${calls}")" -eq 2 ] \ + && grep -- '--extra-vars @' <<< "${calls}" | grep -q 'deploy-genai-gateway.yml' \ + && grep -- '--extra-vars @' <<< "${calls}" | grep -q 'deploy-guardrails.yml'; then + ok "brownfield.sh stack: only the gateway and guard playbooks get an extra vars file" +else fail "brownfield.sh stack: extra vars files: ${calls}"; fi +bf_eat cluster_url=api.example.com deploy_guardrails=on +out=$(bf_run) && rc=0 || rc=$? +if [ "${rc}" -ne 0 ] && grep -qF 'deploy_guardrails=on needs EAT_VLLM_IMAGE_VALUES' <<< "${out}" && ! grep -q '^answers' "${BF}/log"; then + ok "brownfield.sh stack: deploy_guardrails=on without an image overlay stops before EAT runs" +else fail "brownfield.sh stack: guard with no overlay (rc=${rc}): ${out}"; fi +bf_eat cluster_url=api.example.com deploy_guardrails=on +sed -i 's#playbooks/deploy-guardrails.yml#playbooks/deploy-guardrails-renamed.yml#' "${BF}/eat/deploy-agentic-stack.sh" +out=$(bf_run EAT_VLLM_IMAGE_VALUES="${IMG110}") && rc=0 || rc=$? +if [ "${rc}" -ne 0 ] && grep -q 'deploy-guardrails.yml never ran through the pinning wrapper' <<< "${out}"; then + ok "brownfield.sh stack: fails when the guard playbook bypasses the pin" +else fail "brownfield.sh stack: guard bypass not caught (rc=${rc}): ${out}"; fi # A gateway playbook the wrapper never sees (enabled in the config) fails the deploy. -echo 'deploy_genai_gateway=on' >> "${BF}/eat/core/inventory/agentic-config.cfg" +bf_eat cluster_url=api.example.com deploy_genai_gateway=on sed -i 's#playbooks/deploy-genai-gateway.yml#playbooks/deploy-genai-gateway-renamed.yaml#' "${BF}/eat/deploy-agentic-stack.sh" -out=$(env PATH="${BF}/bin:${PATH}" HOME="${BF}/home" STUB_LOG="${BF}/log" EAT_DIR="${BF}/eat" \ - EAT_KUBECONFIG="${WORK}/kubeconfig" K3D_GPU_EAT_DIR="${EAT}" bash "${EAT}/brownfield.sh" 2>&1) && rc=0 || rc=$? -if [ "${rc}" -ne 0 ] && grep -q 'never ran through the pinning wrapper' <<< "${out}"; then +out=$(bf_run) && rc=0 || rc=$? +if [ "${rc}" -ne 0 ] && grep -q 'deploy-genai-gateway.yml never ran through the pinning wrapper' <<< "${out}"; then ok "brownfield.sh stack: fails when the gateway playbook bypasses the pins" else fail "brownfield.sh stack: bypass not caught (rc=${rc}): ${out}"; fi +# Model mode: EAT's xeon-values.yaml merged with the overlays and handed to EAT's +# playbook. On release-1.1.0's values the image overlay is what makes the tags meet; +# without it brownfield.sh stops before EAT's playbook runs. +# bf_model [VAR=value...] +bf_model() { + local fx=$1 + shift + bf_eat cluster_url=api.example.com + cp "${fx}" "${BF}/eat/core/helm-charts/vllm/xeon-values.yaml" + bf_run EAT_MODEL_OPT=off EAT_VLLM_VALUES="${EAT}/vllm-values.yaml" "$@" -- model cpu-qwen3-1-7b +} +MERGED="${BF}/home/xeon-values.k3d-gpu.yaml" +out=$(bf_model "${VM}/xeon-103.yaml") && rc=0 || rc=$? +if [ "${rc}" -eq 0 ] && grep -qx "models cpu-qwen3-1-7b values ${MERGED} inventory bastion-hosts.yaml" "${BF}/log" \ + && [ "$(tag_of "${MERGED}")" = "${PIN103}" ]; then + ok "brownfield.sh model: release-1.0.3-hotfix's values with vllm-values.yaml alone, v0.24.0 by digest" +else fail "brownfield.sh model 1.0.3 (rc=${rc}): ${out} $(cat "${BF}/log")"; fi +out=$(bf_model "${VM}/xeon-110.yaml" EAT_VLLM_IMAGE_VALUES="${IMG110}") && rc=0 || rc=$? +if [ "${rc}" -eq 0 ] && grep -qx "models cpu-qwen3-1-7b values ${MERGED} inventory bastion-hosts.yaml" "${BF}/log" \ + && [ "$(tag_of "${MERGED}")" = "${PIN110}" ]; then + ok "brownfield.sh model: release-1.1.0's values with the image overlay, v0.28.0 by digest" +else fail "brownfield.sh model 1.1.0 (rc=${rc}): ${out} $(cat "${BF}/log")"; fi +rm -f "${MERGED}" +out=$(bf_model "${VM}/xeon-110.yaml") && rc=0 || rc=$? +if [ "${rc}" -ne 0 ] && grep -qF 'v0.24.0' <<< "${out}" && grep -qF 'v0.28.0' <<< "${out}" \ + && ! grep -q '^models ' "${BF}/log" && [ ! -e "${MERGED}" ]; then + ok "brownfield.sh model: release-1.1.0's values without the image overlay are refused, naming both tags" +else fail "brownfield.sh model 1.1.0 without the overlay (rc=${rc}): ${out} $(cat "${BF}/log")"; fi + # 6. eat-opt.py: the model optimizer as values on EAT's own entry, printed against # stock. EAT's playbook and values are reduced to the lines eat-opt.py reads # (deploy-inference-models.yml block for cpu-gemma4-26b-a4b, xeon-values.yaml @@ -617,6 +994,154 @@ if [ "${rc}" -eq 0 ] && cmp -s "${OPT}/stock.yaml" "${OPT}/values.yaml" && grep ok "eat-opt.py: an unknown catalog id leaves the values as merged, with one line" else fail "eat-opt.py: unknown catalog id (rc=${rc}): ${out}"; fi +# 6b. The same on release-1.1.0's values: its 0c726f5 entry for the model (image +# v0.28.0 by tag), merged by vllm-merge.py with vllm-values.yaml and the image +# overlay. eat-opt.py reads vLLM 0.28.0 from the merged tag. The expected choices +# are computed here from console/model_opt.py for vLLM 0.28.0 on the CPU. +cat > "${OPT}/xeon-110.yaml" <<'EOF' +image: + repository: vllm/vllm-openai-cpu + tag: "v0.28.0" + pullPolicy: IfNotPresent +modelConfigs: + "google/gemma-4-26B-A4B-it": + tensor_parallel_size: 1 + pipeline_parallel_size: 1 + configMapValues: + HF_HOME: "/data" + VLLM_CPU_KVCACHE_SPACE: "40" + extraCmdArgs: + [ + "--block-size", "128", + "--dtype", "bfloat16", + "--distributed_executor_backend", "mp", + "--enable_chunked_prefill", + "--enforce-eager", + "--enable-prefix-caching", + "--enable-auto-tool-choice", + "--tool-call-parser", "gemma4", + "--chat-template", "/workspace/vllm/examples/tool_chat_template_gemma4.jinja", + ] +defaultModelConfigs: + tensor_parallel_size: 1 + configMapValues: + VLLM_CPU_KVCACHE_SPACE: "40" + extraCmdArgs: ["--dtype", "bfloat16"] +EOF +if "${EAT}/vllm-merge.py" --kv-gib 6 "${OPT}/xeon-110.yaml" "${OPT}/merged-110.yaml" "${EAT}/vllm-values.yaml" "${IMG110}" 2>&1; then + cp "${OPT}/merged-110.yaml" "${OPT}/merged-110.stock.yaml" + out=$(python3 "${EAT}/eat-opt.py" "${OPT}/playbook.yml" "${OPT}/merged-110.yaml" cpu-gemma4-26b-a4b "${ROOT}/console" \ + --cache "${OPT}/cache" 2>&1) && rc=0 || rc=$? + printf '%s\n' "${out}" > "${OPT}/eat-opt-110.out" + if [ "${rc}" -eq 0 ] && res=$(python3 - "${ROOT}/console" "${OPT}/cache" "${OPT}/merged-110.stock.yaml" \ + "${OPT}/merged-110.yaml" "${OPT}/eat-opt-110.out" 2>&1 <<'PY' +import sys, yaml +console, cache, stock_path, merged_path, out_path = sys.argv[1:] +sys.path.insert(0, console) +import model_opt +mid = "google/gemma-4-26B-A4B-it" +bundle = model_opt.load_or_fetch(mid, cache=cache) +def patch_at(version): + return model_opt.eat_patch(model_opt.optimize(bundle, "vllm", "CPU", {"vllm_version": version, "thinking": "off", "variant": mid})) +patch = patch_at("0.28.0") +# The version is what the explanation turns on: 0.24.0's differs. +assert patch_at("0.24.0")["explain"] != patch["explain"] +out = open(out_path).read() +assert "vLLM 0.28.0 on CPU" in out, out +assert patch["summary"] in out, (patch["summary"], out) +for line in patch["explain"][1:]: + assert (" " + line) in out, (line, out) +eat_args = yaml.safe_load(open(stock_path))["modelConfigs"][mid]["extraCmdArgs"] +entry = yaml.safe_load(open(merged_path))["modelConfigs"][mid] +args = entry["extraCmdArgs"] +# EAT's own arguments first and unchanged, then each optimizer argument EAT lacks. +have = {a for a in eat_args if a.startswith("--")} +want, add, i = [], patch["add_args"], 0 +while i < len(add): + flag = add[i] + value = add[i + 1] if i + 1 < len(add) and not add[i + 1].startswith("--") else None + i += 2 if value is not None else 1 + if flag != "--speculative-config" and flag not in have: + want += [flag] + ([value] if value is not None else []) +assert args == eat_args + want, (args, eat_args + want) +assert ("speculativeConfig" in entry) == bool(patch["speculative_config"]), entry +print("+ " + " ".join(want)) +PY + ); then + ok "eat-opt.py on release-1.1.0's values: vLLM 0.28.0, the console optimizer's 0.28.0 choices (${res})" + else + fail "eat-opt.py on release-1.1.0's values (rc=${rc}): ${res:-} ${out}" + fi +else + fail "eat-opt.py on release-1.1.0's values: vllm-merge.py failed" +fi + +# 7. vllm-merge.py: EAT's xeon-values.yaml <- vllm-values.yaml <- the release's image +# overlay, every KV cache set to --kv-gib. The merged image tag must be the one EAT's +# own file names (the part before '@'), so release-1.1.0's model configs never run on +# v0.24.0 and release-1.0.3-hotfix's never on v0.28.0. +# merge_case +merge_case() { + local name=$1 want=$2 what=$3 out rc + shift 3 + rm -f "${VM}/out.yaml" + out=$("${EAT}/vllm-merge.py" "$@" 2>&1) && rc=0 || rc=$? + if [ "${want}" = ok ] && [ "${rc}" -eq 0 ] && [ "$(tag_of "${VM}/out.yaml")" = "${what}" ]; then + ok "vllm-merge.py: ${name}" + elif [ "${want}" = refused ] && [ "${rc}" -ne 0 ] && grep -qF -- "${what}" <<< "${out}" && [ ! -e "${VM}/out.yaml" ]; then + ok "vllm-merge.py refuses ${name}" + else + fail "vllm-merge.py ${name} (rc=${rc}, want ${want} ${what}): ${out}" + fi +} +cp "${VM}/xeon-103.yaml" "${VM}/xeon-103.orig" +merge_case "release-1.0.3-hotfix + vllm-values.yaml: v0.24.0 by digest" ok "${PIN103}" \ + --kv-gib 6 "${VM}/xeon-103.yaml" "${VM}/out.yaml" "${EAT}/vllm-values.yaml" +if python3 - "${VM}/out.yaml" <<'PY' +import sys, yaml +v = yaml.safe_load(open(sys.argv[1])) +assert v["memory"] == "16Gi" and v["cpu"] == "8" and v["cpu_balloon_annotation"] == "performance", v +assert v["image"]["repository"] == "vllm/vllm-openai-cpu" and v["image"]["pullPolicy"] == "IfNotPresent", v["image"] +q = v["modelConfigs"]["Qwen/Qwen3-1.7B"] +assert q["configMapValues"] == {"VLLM_CPU_KVCACHE_SPACE": "6", "USER": "vllm"}, q +assert q["extraCmdArgs"][-1] == "hermes", q +assert v["defaultModelConfigs"]["configMapValues"]["VLLM_CPU_KVCACHE_SPACE"] == "6", v +assert v["resources"]["requests"]["memory"] == "{{ .Values.memory }}", v +PY +then ok "vllm-merge.py: memory 16Gi, cpu \"8\", balloon performance and KV 6 GiB survive; EAT's keys kept" +else fail "vllm-merge.py: merged values"; fi +cmp -s "${VM}/xeon-103.yaml" "${VM}/xeon-103.orig" && ok "vllm-merge.py: EAT's values file is left as it was" \ + || fail "vllm-merge.py changed EAT's values file" +merge_case "release-1.1.0 + vllm-values.yaml + its image overlay: v0.28.0 by digest" ok "${PIN110}" \ + --kv-gib 6 "${VM}/xeon-110.yaml" "${VM}/out.yaml" "${EAT}/vllm-values.yaml" "${IMG110}" +if python3 - "${VM}/out.yaml" <<'PY' +import sys, yaml +v = yaml.safe_load(open(sys.argv[1])) +assert v["memory"] == "16Gi" and v["cpu"] == "8" and v["cpu_balloon_annotation"] == "performance", v +assert v["modelConfigs"]["Qwen/Qwen3-1.7B"]["configMapValues"]["VLLM_CPU_KVCACHE_SPACE"] == "6", v +assert v["modelConfigs"]["Qwen/Qwen3-1.7B"]["tensor_parallel_size"] == 1, v +PY +then ok "vllm-merge.py: on release-1.1.0's values too, memory, cpu, balloon and KV survive" +else fail "vllm-merge.py: merged release-1.1.0 values"; fi +merge_case "release-1.1.0 + vllm-values.yaml alone (v0.24.0 under v0.28.0's configs)" refused \ + "v0.24.0" --kv-gib 6 "${VM}/xeon-110.yaml" "${VM}/out.yaml" "${EAT}/vllm-values.yaml" +merge_case "release-1.1.0 + vllm-values.yaml alone, naming EAT's tag" refused \ + "v0.28.0" --kv-gib 6 "${VM}/xeon-110.yaml" "${VM}/out.yaml" "${EAT}/vllm-values.yaml" +merge_case "release-1.0.3-hotfix + release-1.1.0's image overlay" refused \ + "v0.24.0" --kv-gib 6 "${VM}/xeon-103.yaml" "${VM}/out.yaml" "${EAT}/vllm-values.yaml" "${IMG110}" +merge_case "no --kv-gib: EAT's own KV cache kept, tag checked" ok "${PIN110}" \ + "${VM}/xeon-110.yaml" "${VM}/out.yaml" "${IMG110}" +python3 -c 'import sys, yaml; v = yaml.safe_load(open(sys.argv[1])); assert v["defaultModelConfigs"]["configMapValues"]["VLLM_CPU_KVCACHE_SPACE"] == "40"' \ + "${VM}/out.yaml" 2>/dev/null && ok "vllm-merge.py: without --kv-gib the KV cache stays EAT's" \ + || fail "vllm-merge.py: KV cache changed without --kv-gib" +merge_case "--require-digest on a tag without a digest" refused \ + "not @sha256:" --require-digest "${VM}/xeon-110.yaml" "${VM}/out.yaml" +merge_case "--require-digest on the image overlay" ok "${PIN110}" \ + --require-digest "${VM}/xeon-110.yaml" "${VM}/out.yaml" "${IMG110}" +printf 'modelConfigs: {}\n' > "${VM}/no-image.yaml" +merge_case "an EAT values file with no image.tag" refused "names no image.tag" \ + "${VM}/no-image.yaml" "${VM}/out.yaml" "${EAT}/vllm-values.yaml" + echo "" echo "${PASS} passed, ${FAILS} failed" [ "${FAILS}" -eq 0 ] From c896719f5447c2d4ce7a38450441387fadc21a9c Mon Sep 17 00:00:00 2001 From: Andrew Mello Date: Thu, 29 Oct 2026 14:24:27 -0700 Subject: [PATCH 2/2] docs(eat), ledger(H1c): EAT release-1.1.0 through eat-up.sh, and H1c pre-registered docs/platforms.md (EAT section), docs/intel.md and integrations/eat/README.md now say what k3d-gpu does with EAT release-1.1.0: eat-up.sh deploys it with EAT_RELEASE=release-1.1.0, the default stays release-1.0.3-hotfix (now a GitHub prerelease), the guardrail is opt-in with its image by digest, the vLLM CPU image is v0.28.0 by digest with a tag check, and the colliding keys are refused. The stale rationale that pending rows pinned 0d3438d through eat-up.sh is replaced: no pending row runs eat-up.sh. Nothing has run against a 1.1.0 stack yet, and no performance number is stated. EXPERIMENTS.md pre-registers H1c, PENDING: H1b's run repeated with EAT_RELEASE=release-1.1.0 on a fresh cluster k3dgpu-h1c-eat, plus three added predictions (the vLLM pod on the v0.28.0 digest, no vllm-guard release, exactly one nri-resource-policy-balloons DaemonSet, k3d-gpu's). A SUPPORTED H1c is the gate for making release-1.1.0 the default. No existing row or section changes; docs/intel/technical-report.md counts 20 PENDING rows. --- EXPERIMENTS.md | 77 +++++++++++++++++++++++++++ docs/intel.md | 8 +-- docs/intel/technical-report.md | 2 +- docs/platforms.md | 83 +++++++++++++++++++---------- integrations/eat/README.md | 95 +++++++++++++++++++++++++++++----- 5 files changed, 221 insertions(+), 44 deletions(-) diff --git a/EXPERIMENTS.md b/EXPERIMENTS.md index 3cd4ce42..f42e47d0 100644 --- a/EXPERIMENTS.md +++ b/EXPERIMENTS.md @@ -22,6 +22,7 @@ setup too weak to decide). Only `SUPPORTED` backs a claim. |---|------------|------------|-------|---------| | H1 | EAT (Intel Enterprise Agent Toolkit) runs on a single non-Xeon machine. | On one host whose CPU is not a Xeon (Core or Core Ultra), `k3d-gpu up` followed by the EAT deploy from `claude/eat-on-k3d` reaches all EAT pods `Ready` and one inference request returns a completion. | Fresh host, Reproduce header filled in. `k3d-gpu up`, deploy EAT, `kubectl wait --for=condition=Ready pod --all -A --timeout=30m`, then one completion request against the EAT endpoint. Refuted by any pod not Ready at the timeout, a Xeon-only instruction or node-label requirement, or no completion. Wave 1 (2026-10-16, i9-13900T): a completion returned, but the probe was not run as written (see [H1 run](#h1-run-i9-13900t-2026-10-16)). Amended 2026-10-17T20:27Z, before run 2: matched load instead of a fresh host, one fresh cluster, every step timed and logged, grading of Job pods fixed in advance. Run 2 (2026-10-17): every step held in one pass except the `--all -A` wait, which no k3s v1.37 cluster can pass (k3s's Gateway API CRD Job leaves a Succeeded pod); see [H1 run 2](#h1-run-2-i9-13900t-2026-10-17). | INCONCLUSIVE | | H1b | EAT (Intel Enterprise Agent Toolkit) runs on a single non-Xeon machine (H1's hypothesis, re-run with a readiness probe that can pass on k3s v1.37). | On one host whose CPU is not a Xeon (Core or Core Ultra), `k3d-gpu up` followed by the EAT deploy reaches every pod that is not in phase Succeeded `Ready` within 30 min, and one inference request returns a completion. | Pre-registered 2026-10-17T21:13Z (093ba98), before any H1b step; H1 keeps its INCONCLUSIVE verdict. One fresh cluster `k3dgpu-h1b-eat`, scratch `KUBECONFIG`, empty `EAT_WORK`, under `bench/load/load.py` `typical` on the E-cores (16-31) with 60 s load samples; the documented user path run once, each step timed: `k3d-gpu up`, `eat-up.sh`, `eat-up.sh model cpu-qwen3-1-7b`, `kubectl wait --for=condition=Ready pod -A --field-selector=status.phase!=Succeeded --timeout=30m`, one completion through EAT's LiteLLM gateway, `eat-check.sh`, `k3d-gpu down`. Every Job's completion is recorded on its own line and not graded. Refuted by any step 1-3 failing, a wait that ends non-zero with a Deployment, StatefulSet, DaemonSet, ReplicaSet or Sandbox pod not Ready, a Xeon-only instruction or node-label requirement, or no completion. Grading, lock budget and outcome table in [H1b pre-registration](#h1b-pre-registration-i9-13900t-2026-10-17). Run (2026-10-17, i9-13900T, k3s v1.37.1): steps 1-3 rc 0, the wait exited 0 after 280.1 s with all 33 selected pods Ready, both Jobs `Complete`, completion HTTP 200; see [H1b run](#h1b-run-i9-13900t-2026-10-17). | SUPPORTED | +| H1c | EAT (Intel Enterprise Agent Toolkit) runs on a single non-Xeon machine at EAT's current release, `release-1.1.0` (H1b's hypothesis, deployed through `eat-up.sh` with `EAT_RELEASE=release-1.1.0`). | H1b's prediction, plus three: the vLLM pod runs `vllm/vllm-openai-cpu@sha256:197354e6475e638eb5ccca09beb078488046a82de0e204e7429bf1a804df9647` (v0.28.0); no `vllm-guard` Helm release exists (shipped config, `deploy_guardrails=off`); exactly one `nri-resource-policy-balloons` DaemonSet exists, k3d-gpu's. | Pre-registered 2026-10-29, before any H1c step; H1b keeps its verdict. H1b's protocol, background load, steps, grading, lock budget and outcome table ([H1b pre-registration](#h1b-pre-registration-i9-13900t-2026-10-17)), with `EAT_RELEASE=release-1.1.0` (EAT `0c726f57`) and one fresh cluster `k3dgpu-h1c-eat`; the three added predictions are read as step 4p, after the Job lines and before the completion. Refuted by any of H1b's refuting conditions or by any added prediction failing. A SUPPORTED H1c is the gate for making `release-1.1.0` `eat-up.sh`'s default. See [H1c pre-registration](#h1c-pre-registration-i9-13900t-2026-10-29). | PENDING | | H2 | On an Intel hybrid host whose only Intel GPU is an iGPU (wave 1: i9-13900T, UHD 770), the iGPU serves a fixed model faster and with less energy per token than the CPU running the same model with a tuned thread count. | iGPU decode tok/s is higher than the best CPU tok/s, and iGPU J/token is lower than the CPU J/token at that best thread count. | Row restated 2026-10-16 to the pre-registration that ran (written ~10:20 PDT, before the 10:29-11:32 PDT campaign); the row first committed at 04:48 PDT named a Core Ultra host, which was not tested. OVMS 2026.4.1 with `OpenVINO/Qwen3-4B-int4-ov`, same digest and graph except the device. CPU baseline swept over 4 thread and core configs first, best kept. Energy from RAPL `package-0`, with a 20 s model-loaded idle before each block. Decode 30 x 256, prefill ~1936 x 16, 4-in-flight 30 x 128; 5 rounds, ABBA order. Refuted if the iGPU loses on either tok/s or J/token. | REFUTED | | H3 | P/E pinning isolates inference from noisy neighbours on a hybrid CPU. | With the inference pod labelled `k3d-gpu.io/cores: performance` and a CPU-saturating neighbour labelled `k3d-gpu.io/cores: efficient`, inference latency stays within 10% of the no-neighbour baseline on both decode and prefill. Without pinning, the same neighbour raises latency by more than 10% on at least one workload. | Pre-registered 2026-10-16 ~13:20 PDT, before any timed block (13:24-14:53 PDT). OVMS on CPU, Qwen3-4B int4, 15-CPU request; neighbour `bench/noisy.sh` (stress-ng `--cpu 15`, Guaranteed 15 CPU). Arms: Q pinned and quiet, P pinned with the neighbour on the E-cores, U both unlabelled, plus Qu, Qe, X (roles swapped) and PM (stream neighbour). `Cpus_allowed_list` read in every block. 5 rounds x 7 arms, 5 requests per workload per block. Refuted if P is more than 10% above Q on either workload. Run 2026-10-16 20:24Z..21:53Z, provenance header and Reproduce block in [H3 run](#h3-run-i9-13900t-2026-10-16). | REFUTED | | H4 | The automatic device choice equals the measured best device. | For a fixed model on a host with more than one usable device (CPU, iGPU, NPU, dGPU), the device the agent stack selects automatically (`claude/agent-stack`) is the one with the highest measured tok/s in a sweep over every usable device. | Record the auto-selected device. Then force each usable device in turn and measure tok/s with the H2 method. Refuted if any forced device beats the auto choice by more than the run-to-run spread. Wave 1 pre-registered 2026-10-16 15:30 PDT (H4b) and 16:22 PDT (H4c), each before its runs, against the `auto` rule of `claude/agent-stack` 8385dc2. Run 2026-10-16T22:27Z..2026-10-17T01:12Z, arms G = `auto` (UHD 770), C16, C8 and Ce; provenance header and Reproduce block in [H4 run](#h4-run-i9-13900t-2026-10-16). | REFUTED | @@ -9446,6 +9447,82 @@ integrations/eat/eat-check.sh # expect: ==> E scripts/k3d-gpu down; bench/load/load.py stop ``` +### H1c pre-registration (i9-13900T, 2026-10-29) + +Written 2026-10-29 (UTC) with the change that adds `EAT_RELEASE` to `eat-up.sh`, before +any H1c step. H1b keeps its SUPPORTED verdict, which is for `release-1.0.3-hotfix`. H1c is +a new row, run once, and graded only by what is written here and in +[H1b's pre-registration](#h1b-pre-registration-i9-13900t-2026-10-17). + +Why a new row: EAT published `release-1.1.0` on 2026-10-22 (`0c726f576595971c53cc45b94e08ba969beb4a4a`, +the merge of EAT #21, +https://github.com/intel/enterprise-agent-toolkit/releases/tag/release-1.1.0), and GitHub +now flags `release-1.0.3-hotfix` as a prerelease. `eat-up.sh` deploys `release-1.1.0` with +`EAT_RELEASE=release-1.1.0`; its default stays `release-1.0.3-hotfix` until this row is +SUPPORTED. On `release-1.1.0`, `eat-up.sh` and `brownfield.sh` add three things H1b never +exercised: the vLLM CPU image `v0.28.0` from `vllm-image-release-1.1.0.yaml` with the tag +check in `vllm-merge.py`, `DEPLOY_GUARDRAILS` taken from the config (shipped `off`, while +1.1.0 itself defaults it on), and the refusal of EAT's balloons policy. The added +predictions read those three on the live cluster. + +- **As H1b.** The hypothesis, the host, the background load (`bench/load/load.py` `typical` + on the E-cores 16-31, 60 s samples), one fresh cluster with a scratch `KUBECONFIG` and an + empty `EAT_WORK`, steps 0-7 each run once, timed and logged, the host lock with its + 25 min budget and the stop of step 4 at 21 min, the grading and the outcome table. +- **Changes.** Steps 0, 2 and 3 run with `EAT_RELEASE=release-1.1.0`, so the clone is at + `0c726f57` (`eat-up.sh` refuses any other HEAD). The cluster is `k3dgpu-h1c-eat`. The + config is `integrations/eat/agentic-config.cfg` as shipped at the run's commit, which + sets `deploy_guardrails=off` and `deploy_nri_balloon_policy=off`. +- **Step 4p, after step 4j and before step 5.** Each command and its output recorded + verbatim: + - P1, the vLLM image. `kubectl get pod -n default -l app.kubernetes.io/instance=vllm-qwen-3-1-7b-cpu + -o jsonpath='{.items[*].spec.containers[*].image} {.items[*].status.containerStatuses[*].imageID}'` + shows the image `vllm/vllm-openai-cpu:v0.28.0@sha256:197354e6475e638eb5ccca09beb078488046a82de0e204e7429bf1a804df9647` + and an `imageID` that ends in `@sha256:197354e6475e638eb5ccca09beb078488046a82de0e204e7429bf1a804df9647`. + - P2, no guard. `kubectl get secrets -A -l owner=helm,name=vllm-guard -o name` prints + nothing, and `kubectl get namespace guardrails` exits non-zero. + - P3, one balloons policy. `kubectl get daemonsets -A -o json | jq '[.items[] | + select(.metadata.name | startswith("nri-resource-policy"))] | map(.metadata.namespace + "/" + .metadata.name)'` + prints exactly `["kube-system/nri-resource-policy-balloons"]`, and + `kubectl get secrets -n kube-system -l owner=helm,name=nri-resource-policy-balloons -o name` + prints nothing. EAT's role would install that DaemonSet as Helm release + `nri-resource-policy-balloons`; k3d-gpu's comes from k3s's auto-deploy manifest, + which leaves no Helm release. +- **Grading, fixed now.** SUPPORTED only if every SUPPORTED condition of H1b holds and P1, + P2 and P3 hold. REFUTED by any of H1b's refuting conditions, or by any of P1-P3 failing, + whatever step 4 says. INCONCLUSIVE in H1b's INCONCLUSIVE cases, provided P1-P3 were read + and hold; P1-P3 not read because the lock ran out is INCONCLUSIVE. +- **Prediction.** SUPPORTED, prior 0.55. The stubs in `tests/eat_test.sh` cover the + release check, the config checks, the vLLM merge and the guard switch, but not + `release-1.1.0`'s own brownfield path against a live cluster (its prerequisite checks + and bastion playbooks changed in #21). That, and the pull of a new vLLM image inside the + lock budget, are the main risks. + +```bash +# Reproduce on one fresh cluster under the typical load. Host needs docker, k3d, kubectl, curl, jq. +lscpu | grep 'Model name'; uname -r; git rev-parse HEAD +export KUBECONFIG=$PWD/h1c.kubeconfig K3D_GPU_CLUSTER=k3dgpu-h1c-eat EAT_WORK=$PWD/eat-work EAT_RELEASE=release-1.1.0 +IMAGE=k3d-gpu:eat-$(git rev-parse --short HEAD) +docker build -q -t "$IMAGE" . && docker image inspect --format '{{.Id}}' "$IMAGE" +bench/load/load.py start --level typical --ttl 1800 +bench/load/load.py sample --seconds 60 --windows 26 --tag h1c --level typical --out load.jsonl & +K3D_GPU_IMAGE="$IMAGE" \ +K3D_GPU_NODE_LABELS=ei-infra-eligible=true,ei-inference-eligible=true \ +K3D_GPU_K3S_ARGS=--disable=servicelb scripts/k3d-gpu up +integrations/eat/eat-up.sh +integrations/eat/eat-up.sh model cpu-qwen3-1-7b +kubectl wait --for=condition=Ready pod -A --field-selector=status.phase!=Succeeded --timeout=30m +kubectl get jobs -A +kubectl get pod -n default -l app.kubernetes.io/instance=vllm-qwen-3-1-7b-cpu \ + -o jsonpath='{.items[*].spec.containers[*].image} {.items[*].status.containerStatuses[*].imageID}' # P1 +kubectl get secrets -A -l owner=helm,name=vllm-guard -o name; kubectl get namespace guardrails # P2 +kubectl get daemonsets -A -o json \ + | jq '[.items[] | select(.metadata.name | startswith("nri-resource-policy"))] | map(.metadata.namespace + "/" + .metadata.name)' +kubectl get secrets -n kube-system -l owner=helm,name=nri-resource-policy-balloons -o name # P3 +integrations/eat/eat-check.sh # expect: ==> EAT check passed. +scripts/k3d-gpu down; bench/load/load.py stop +``` + ### H2 run (i9-13900T, 2026-10-16) ```text diff --git a/docs/intel.md b/docs/intel.md index 684d04fe..e6d6cca1 100644 --- a/docs/intel.md +++ b/docs/intel.md @@ -463,9 +463,11 @@ hardware, a recorded run. runs gave the `xe`, DSA, IAA, QAT and AMX rows their first hardware results and the CPU ISA labels their first recorded live node, and both ledger rows are REFUTED [ledger:H52] [ledger:H52b]; the first Gemma 4 rows are recorded, none SUPPORTED yet. -- **EAT 1.1.0.** The `release-1.1.0` tag is cut (2026-10-22). Moving `eat-up.sh` to it - waits until no pending ledger row pins `0d3438d`, then needs the checks listed in - [platforms.md](platforms.md#intel-enterprise-agent-toolkit-eat). +- **EAT 1.1.0 by default.** `eat-up.sh` deploys `release-1.1.0` (2026-10-22) on request, + with `EAT_RELEASE=release-1.1.0` and the checks listed in + [platforms.md](platforms.md#intel-enterprise-agent-toolkit-eat); the default stays + `release-1.0.3-hotfix`. It moves after a fresh run on 1.1.0, EXPERIMENTS.md H1c + (pending), passes. Nothing has run against a 1.1.0 stack through `eat-up.sh` yet. - **NPU telemetry.** No NPU exporter is shipped. - **Upstream.** Small, separate fixes for running EAT and its successor installer on k3s: accept `+k3s` versions in platform detection, pass Istio's diff --git a/docs/intel/technical-report.md b/docs/intel/technical-report.md index 48b1d3e1..0e88d786 100644 --- a/docs/intel/technical-report.md +++ b/docs/intel/technical-report.md @@ -11,7 +11,7 @@ This report covers what was measured, and how. Every result but H52, H52b, H25x, - Seventeen hypotheses are SUPPORTED. Sixteen are scoped to that host; H25x is H25 replicated on the Xeon 6741P. H57dm (the Gemma 4 QAT MTP drafter speeds llama.cpp decode on the P-cores) is the newest. - Seventeen are REFUTED. Six of them changed shipped defaults. The newest, H52b, is H52 after `k3d-gpu host-setup --apply` on the Xeon 6741P: DSA was advertised and reached a pod, but IAA got no work queue, the QAT plugin advertised none of the VFs, and `up` advertised two of the four B70s: the serving pairs hold about half the VRAM of every card, and its busy-card filter (half or more) left out the two just over half and kept the two just under it [ledger:H52b]. Before it, H51 ran the graded agents-per-box ramp on the Xeon 6741P: the k3d-gpu CPU stack failed at one agent (turn p95 10.41 s against the 10 s SLO), and so did EAT's stock vLLM CPU arm [ledger:H51]. H50 ran inside EAT's banking flow on the same host: EAT's current stock vLLM CPU arm (release-1.1.0) delivered at least as many tasks within the turn budget as the k3d-gpu CPU stack: 21 of 24 against 19 of 24 [ledger:H50]. - 28 are INCONCLUSIVE, mostly because the shared host throttles or its load was unmatched. H50o (the k3d-gpu CPU stack against EAT's own OVMS chart on the Xeon 6741P) is INCONCLUSIVE because that chart's OVMS 2025.4 crashed on loading Gemma 4 26B-A4B, so no pair ran. H54a and H54d (Gemma 4 26B-A4B QAT on the Xeon 6741P) are INCONCLUSIVE because the stock arm, EAT 1.0.3's vLLM CPU v0.24.0, as this harness rendered EAT's entry (no CPU balloon, requests or limits), stalled in both of H54a's holds, so no arm could be scored against it. EAT 1.1.0's vLLM CPU v0.28.0 was not measured in H54a; H50 ran it in EAT's banking flow. H54b and H54c (one Arc Pro B70 against two, and experts in DRAM with the rest on one B70) are INCONCLUSIVE because their "one card" pods saw all four cards, so no arm was delivered as pre-registered. H57da (k3d-gpu's OVMS pick vs llama.cpp for Gemma 4 QAT on the i9-13900T) did not resolve: the strictly pinned llama.cpp arms tracked host load; H57db did not run (memory). H28x (H28 replicated on the Xeon 6741P) is INCONCLUSIVE: its one pre-registered hold failed in setup and measured nothing; one re-run is registered. H56 (two holds at once in cpuset slices of the Xeon 6741P) is INCONCLUSIVE: in every hold with a sibling, the sibling's banking block ended shortly after the measured block began, so all four were VOID and no interference statistic was scored; crescent rows stay at one hold at a time. -- 19 are PENDING: the i9-13900T thread-pinning follow-ups H57dp and H57de, the Xeon 6741P rows H53, H54q, H60 (Qwen3.8-Flash-Next from host DRAM) and the zero-touch thesis row H58, the cross-host replication H7x, the engine-choice rows H59 (Xeon 6741P), H59d (i9-13900T) and H59w (i7-13700), H55 (SLO-gated admission of an E-core neighbour on the i9-13900T), and the second client host's (wildct, i7-13700) rows H52w, H18w, H25w, H28w, H34w, H57wa, H57wm and H57wb, pre-registered and not yet run. H52, the first row run on that host (a Xeon 6741P with four Arc Pro B70s), is REFUTED. H27x, H27 re-run on the Xeon (AMX), is REFUTED as on the i9. H34x, H34 re-run there, is REFUTED: the CPU Fast Draft that sped up the i9 slowed decode on the Xeon. +- 20 are PENDING: H1c (H1b's EAT run repeated on EAT `release-1.1.0`, i9-13900T), the i9-13900T thread-pinning follow-ups H57dp and H57de, the Xeon 6741P rows H53, H54q, H60 (Qwen3.8-Flash-Next from host DRAM) and the zero-touch thesis row H58, the cross-host replication H7x, the engine-choice rows H59 (Xeon 6741P), H59d (i9-13900T) and H59w (i7-13700), H55 (SLO-gated admission of an E-core neighbour on the i9-13900T), and the second client host's (wildct, i7-13700) rows H52w, H18w, H25w, H28w, H34w, H57wa, H57wm and H57wb, pre-registered and not yet run. H52, the first row run on that host (a Xeon 6741P with four Arc Pro B70s), is REFUTED. H27x, H27 re-run on the Xeon (AMX), is REFUTED as on the i9. H34x, H34 re-run there, is REFUTED: the CPU Fast Draft that sped up the i9 slowed decode on the Xeon. - Every other Intel family is detected and configured in fixture tests only. None has a measurement. No world-first, superlative or priority claim is made; [docs/provenance.md](../provenance.md) supports none. diff --git a/docs/platforms.md b/docs/platforms.md index 4c59d819..fdf7e07f 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -220,30 +220,38 @@ Simulation fixture: `tests/fixtures/xeon-dmr` (topology borrowed from EAT is a software stack, not a hardware family, so it has no fixture. Its pins: -- `integrations/eat/eat-up.sh` deploys the pinned tag `release-1.0.3-hotfix` - ([`0d3438d`](https://github.com/intel/enterprise-agent-toolkit/tree/0d3438d7119de44cafc5903f66665c0b623e3333)). - The latest tag is [`release-1.1.0`](https://github.com/intel/enterprise-agent-toolkit/releases/tag/release-1.1.0) +- `integrations/eat/eat-up.sh` deploys `release-1.0.3-hotfix` + ([`0d3438d`](https://github.com/intel/enterprise-agent-toolkit/tree/0d3438d7119de44cafc5903f66665c0b623e3333)) + by default, and [`release-1.1.0`](https://github.com/intel/enterprise-agent-toolkit/releases/tag/release-1.1.0) ([`0c726f5`](https://github.com/intel/enterprise-agent-toolkit/tree/0c726f576595971c53cc45b94e08ba969beb4a4a), - published 2026-10-22, [tags](https://api.github.com/repos/intel/enterprise-agent-toolkit/tags)), - the merge of [PR #21](https://github.com/intel/enterprise-agent-toolkit/pull/21). Its tree, - `04969afa`, is the same as that of #21's head `c86c42b`. + published 2026-10-22, [tags](https://api.github.com/repos/intel/enterprise-agent-toolkit/tags)) + with `EAT_RELEASE=release-1.1.0`. Each release is checked out at its commit only, and + an `EAT_WORK` cloned for one is refused for the other. `release-1.1.0` is the merge of + [PR #21](https://github.com/intel/enterprise-agent-toolkit/pull/21) and Intel's latest + release. Its tree, `04969afa`, is the same as that of #21's head `c86c42b`. GitHub now + flags `release-1.0.3-hotfix` as a prerelease + ([releases](https://api.github.com/repos/intel/enterprise-agent-toolkit/releases), read 2026-10-29). - `docs/intel.md`, `contrib/eat/` and `integrations/eat/upstream/` read `main` at [`0ecf67a`](https://github.com/intel/enterprise-agent-toolkit/tree/0ecf67a8331e8b502ef41e08bf6d2881416178c5). -`eat-up.sh` and `brownfield.sh` stay on `release-1.0.3-hotfix` while pre-registered -ledger rows pin EAT stock at `0d3438d` (H50, H50o and H51, and `bench/apb/PROTOCOL.md` -through `eat-up.sh`). Moving them to `release-1.1.0` still needs the two checks -described below: `brownfield.sh` has to export `DEPLOY_GUARDRAILS=off`, and the vLLM -CPU image has to be re-pinned to `v0.28.0` by digest. A fresh run like H1b's then has -to pass on it [ledger:H1b]. +The default stays `release-1.0.3-hotfix` until a fresh run passes on `release-1.1.0`: +EXPERIMENTS.md H1c (pending) repeats H1b's run on it [ledger:H1b], and a SUPPORTED H1c +is the gate for making `release-1.1.0` the default. No pending ledger row runs +`eat-up.sh`. H50, H50o and H51 are decided, and ran EAT stock at `release-1.1.0` from +their own harness (crescent amendment E1). `bench/apb/PROTOCOL.md`'s `eat-up.sh` arm +belongs to H8, also decided. Nothing has run against a `release-1.1.0` stack through +`eat-up.sh` yet. `release-1.1.0` ships NVIDIA OpenShell as an opt-in component (`deploy_openshell=off` by default, [PR #14](https://github.com/intel/enterprise-agent-toolkit/pull/14)). -The pinned tag has no OpenShell role, so `eat-up.sh` stops on a config that sets -`deploy_openshell=on` instead of letting the tag skip the key. Upstream lists a CNI -that enforces `NetworkPolicy` and best-effort Landlock as OpenShell requirements +`release-1.0.3-hotfix` has no OpenShell role, so on it `eat-up.sh` stops on a config that +sets `deploy_openshell=on` instead of letting the release skip the key. With +`EAT_RELEASE=release-1.1.0` the key passes to EAT, whose `fresh-install.sh` also requires +`deploy_agent_sandbox=on` (the shipped config has it). Upstream lists a CNI that enforces +`NetworkPolicy` and best-effort Landlock as OpenShell requirements ([docs/openshell.md](https://github.com/intel/enterprise-agent-toolkit/blob/0ecf67a8331e8b502ef41e08bf6d2881416178c5/docs/openshell.md)). -k3d-gpu adds no OpenShell checks while `eat-up.sh` stays on the pinned tag. +k3s enforces `NetworkPolicy` by default. k3d-gpu adds no other OpenShell check, and +OpenShell on k3d-gpu is UNVERIFIED. Upstream work that changes this integration: @@ -274,7 +282,7 @@ Read with `gh`, `git ls-remote` and a read-only clone only. EAT `main` is `4a2e9 | [#5](https://github.com/intel/enterprise-agent-toolkit/pull/5) Bitnami to SolDevelo images | `fcec7ef`, rebased onto `main` | `bitnami/kubectl:latest` becomes `soldevelo/kubectl:1.29` in the GenAI Gateway restore job, and `bitnamilegacy/minio:2024.12.18` becomes `soldevelo/minio:2024.12.18` in `docker/docker-compose.yml` | None. Neither line runs on our path: `restoreJob.enabled` is `false`, and `eat-up.sh` does not use EAT's Docker Compose mode. Every Bitnami image our path renders pulled on 2026-10-18 (`docker manifest inspect`, amd64 and arm64). See the note below | None. Recheck if a `bitnamilegacy` pull fails | | [#19](https://github.com/intel/enterprise-agent-toolkit/pull/19) unified convention | Merged 2026-10-27 into `release-2.0.0`, not `main`: final head `fb59459`, merge `036b662`, no tag | Deletes `deploy-agentic-stack.sh`, `core/lib` (the brownfield path with it), `core/playbooks` and `core/roles`; installs through EAS `es_auto_installer.sh`. `release-2.0.0`'s README says Intel Arc (XPU) deployment is not in that release | Ends the path `eat-up.sh` uses, once a tag ships it. The drafted EAT docs patch in [upstream-eat.md](intel/upstream-eat.md) passes `git apply --check` on the draft head `9d842f5` but not on `fb59459` or `036b662`, so it needs a re-base before it is sent | Keep `eat-up.sh` on the tag. Move to the EAS BYO path after a tag that includes #19 | | [#20](https://github.com/intel/enterprise-agent-toolkit/pull/20) OpenClaw operator | `10deb6d`, on #19's branch `agentic-toolkit-migration` (forked at `dab3a98`, before #19's final head) | Optional `openclaw_operator` role (`claw_enabled: false`): operator chart and image `0.38.3` pinned by digest, 3 CRDs (`openclaw.rocks/v1alpha1`). It also adds a `cosign verify` of the chart and the operator image when `cosign` is on the installer's `PATH` (`openclaw_operator_cosign_required: false` by default) | Not reachable from the pinned tag. The operator itself fits one k3d-gpu node: CPU only (100m / 128Mi request), no GPU, no webhook, `kubeVersion >=1.28`, and both images publish amd64. Its example instance is the gap (see below) | None until #19 and #20 reach a tag | -| [#21](https://github.com/intel/enterprise-agent-toolkit/pull/21) Release 1.1.0 | Merged 2026-10-22 as `0c726f5` and released as `release-1.1.0`; head `c86c42b`, 204 files against `release-1.0.3-hotfix` | Keeps the old layout (`deploy-agentic-stack.sh`, `core/lib`); adds Qwen3Guard guardrails (on by default), Kata sandboxes (`kata-fc` Firecracker and `kata-qemu`), vLLM Semantic Router, rack-scale mode, an NVIDIA B300 profile, and an Arc B60/B70 XPU path (`compute_platform=xpu`); OVMS as a Docker engine; vLLM CPU image `v0.24.0` to `v0.28.0`; NRI balloons role reworked into NUMA-aware per-node policies | None on the pinned tag. On `release-1.1.0`: `core/lib/brownfield/` and `deploy-genai-gateway.yml` are unchanged, every function `brownfield.sh` calls still exists, and `gateway-charts.py` runs clean on its charts. Two changes would bite, and the XPU path would overlap what k3d-gpu already installs; all read from source, not run (see below) | Before moving `eat-up.sh` to `release-1.1.0`: turn the guardrail off and re-pin the vLLM image. Validation on 4x Arc Pro B70 is in progress, no results yet | +| [#21](https://github.com/intel/enterprise-agent-toolkit/pull/21) Release 1.1.0 | Merged 2026-10-22 as `0c726f5` and released as `release-1.1.0`; head `c86c42b`, 204 files against `release-1.0.3-hotfix` | Keeps the old layout (`deploy-agentic-stack.sh`, `core/lib`); adds Qwen3Guard guardrails (on by default), Kata sandboxes (`kata-fc` Firecracker and `kata-qemu`), vLLM Semantic Router, rack-scale mode, an NVIDIA B300 profile, and an Arc B60/B70 XPU path (`compute_platform=xpu`); OVMS as a Docker engine; vLLM CPU image `v0.24.0` to `v0.28.0`; NRI balloons role reworked into NUMA-aware per-node policies | `eat-up.sh` deploys it with `EAT_RELEASE=release-1.1.0`; the default stays `release-1.0.3-hotfix`. `core/lib/brownfield/` and `deploy-genai-gateway.yml` are unchanged, every function `brownfield.sh` calls still exists, and `gateway-charts.py` runs clean on its charts. The guardrail default and the vLLM image would have changed the stack, and the XPU path, Kata and EAT's balloons would collide with a k3d-gpu cluster; all read from source, not run (see below) | Done in `eat-up.sh` and `brownfield.sh`: Qwen3Guard opt-in with its image by digest, vLLM CPU `v0.28.0` by digest with a tag check, and the colliding keys refused before the cluster is touched. The default moves to `release-1.1.0` after EXPERIMENTS.md H1c (pending). Validation on 4x Arc Pro B70 is in progress, no results yet | | [#22](https://github.com/intel/enterprise-agent-toolkit/pull/22)-[#26](https://github.com/intel/enterprise-agent-toolkit/pull/26) agent toolkit stack | `a627fad`, `a32c5aa`, `9edf77d`, `6dfbcbc`, `7cc4344`, stacked on #20, base `agentic-toolkit-migration` | `Agent`, `AgentGrant` and `Tool` CRDs (`intel-stack.io`) with an agent operator (#22); per-agent memory (#23); sandbox, GitHub and web tools for agents (#24); an agent REST API (#25); a browser console and agent dashboards (#26). Its images are built into an in-cluster registry (`registry.kube-system.svc.cluster.local:5000`, the `kubernetes_registry_enabled` add-on) | Not reachable from the pinned tag. The stack claims sandboxes through `SandboxClaim` at `extensions.agents.x-k8s.io/v1beta1`, the version k3d-gpu's agent-sandbox v1.0.7 serves. Read from source, not run | None until the stack reaches a tag | Bitnami images on the pinned path. `deploy-genai-gateway.yml` runs `helm dependency update` @@ -318,17 +326,35 @@ clone; nothing here has run against a 1.1.0 stack. (`custom_hybrid_router.proxy_handler_instance`), loaded from a ConfigMap, not a new image. - Guardrail on by default. `deploy-agentic-stack.sh` sets `DEPLOY_GUARDRAILS="${DEPLOY_GUARDRAILS:-on}"` at top level (line 195), which runs when `brownfield.sh` sources it. The `deploy_guardrails` - key in `agentic-config.cfg` is read only inside `main()` (line 1138), which a sourced run - skips. `fresh-install.sh` (line 419) then deploys the `Qwen/Qwen3Guard-Gen-0.6B` guard. - `brownfield.sh` would have to export `DEPLOY_GUARDRAILS=off` to keep today's stack. -- vLLM image. #21's `xeon-values.yaml` (line 34) moves `vllm/vllm-openai-cpu` from - `v0.24.0` to `v0.28.0`. `vllm-values.yaml` merges over it with no tag check, so it would - keep `v0.24.0` by digest under 1.1.0's model configs. Re-pin it with the bump. + key in `agentic-config.cfg` is read only inside `main()` (lines 1136-1140), which a sourced + run skips. `fresh-install.sh` (line 419) then deploys the `Qwen/Qwen3Guard-Gen-0.6B` guard. + What k3d-gpu does: `brownfield.sh` exports `DEPLOY_GUARDRAILS` from the config's + `deploy_guardrails` before it sources the script, `off` unless the key is `on` or `yes`, + and the shipped `agentic-config.cfg` sets `deploy_guardrails=off`. So Qwen3Guard is + opt-in. With `deploy_guardrails=on`, `deploy-guardrails.yml` alone gets + `remote_helm_charts_base` as an extra var, pointing at a copy of EAT's `vllm` chart whose + `guardrail-values.yaml` names `vllm/vllm-openai-cpu:v0.28.0` by digest. In that playbook + the variable serves only the guard's chart and values (lines 115, 133, 135). A marker + file fails the deploy if the playbook bypasses the copy, as for the gateway. + `eat-up.sh` refuses `deploy_guardrails=on` on `release-1.0.3-hotfix`, which has no + guardrail role. +- vLLM image. #21's `xeon-values.yaml` (line 34) and `guardrail-values.yaml` (line 30) move + `vllm/vllm-openai-cpu` from `v0.24.0` to `v0.28.0`, by tag only. v0.28.0 contains + [vllm#45844](https://github.com/vllm-project/vllm/pull/45844) (`26eb8720`, the Gemma 4 + split-KV fix), and v0.24.0 does not (EXPERIMENTS.md crescent amendment E1). What k3d-gpu + does: for `release-1.1.0`, `eat-up.sh` mounts `vllm-image-release-1.1.0.yaml` (v0.28.0 at + its index digest `sha256:197354e6...`), which `brownfield.sh` merges last. + `vllm-merge.py` refuses any merge whose tag, up to the `@`, differs from the one in EAT's + own file. So no path serves 1.1.0's model configs on v0.24.0, or 1.0.3's on v0.28.0. + `vllm-values.yaml` keeps its `v0.24.0` pin: `bench/apb` merges it for memory, CPU and the + balloon, and sets its own image. - New `agentic-config.cfg` keys (`deploy_kata_containers`, `kata_runtime`, `deploy_ceph`, `rack_scale_mode`, `deploy_guardrails`, `deploy_vllm_sr`, `deploy_openshell`) are all opt-in except `deploy_guardrails`. The NRI balloons role is reworked (`install_nri.yaml` - removed); `eat-up.sh` keeps `deploy_nri_balloon_policy=off`, so k3d-gpu's policy stays the - only one. + removed). `eat-up.sh` refuses any `deploy_nri_balloon_policy` but `off` or `no`, on both + releases, so k3d-gpu's policy stays the only one: both releases turn EAT's on when the + key is absent. On `release-1.1.0` it also refuses `deploy_kata_containers=on` and + `rack_scale_mode=on`, which builds its own cluster from `rack-profile.yaml`. - #21 does not contain #19: it keeps `deploy-agentic-stack.sh`, which #19 deletes. #21's XPU path (`compute_platform=xpu`, `docs/intel-arc-xpu-deployment.md`), set against @@ -347,15 +373,18 @@ B70 runs land: - Expected collision: a second GPU plugin. `deploy-intel-gpu-plugin.yml` applies `deployments/gpu_plugin/base/intel-gpu-plugin.yaml` from intel-device-plugins `main` (`intel_gpu_plugin_ref` defaults to `main`, unpinned) beside the baked DaemonSet. + `eat-up.sh` therefore refuses `compute_platform=xpu` (or `x`) on `release-1.1.0` and + points to `K3D_GPU_AGENT_BACKEND=vllm` for vLLM on an Intel GPU. - Expected collision: a second balloons policy. The reworked `nri_cpu_balloons` role renders one NUMA-sized `BalloonsPolicy` per node (`node.`). #21 still installs - it only on `deploy_nri_balloon_policy == "yes"` (`fresh-install.sh` line 247), so - `eat-up.sh`'s `off` keeps it away; a config that turns it on meets k3d-gpu's policy. + it only on `deploy_nri_balloon_policy == "yes"` (`fresh-install.sh` line 247), and + `eat-up.sh` refuses every value but `off` or `no`. - Expected gap: the `hostPath` libraries resolve inside the k3d node container, not on the host. The node image would have to carry compatible Level Zero and OpenCL user space at those paths, or the pod would mount empty or mismatched libraries. - Expected gap: Kata (`kata-fc`, `kata-qemu`) needs `/dev/kvm` and nested virtualization inside the k3d node, which k3d-gpu does not pass through today. + `eat-up.sh` refuses `deploy_kata_containers=on` for that reason. Gaps for the #20 example instance (`docs/examples/openclaw-instance.yaml`) on k3d-gpu: diff --git a/integrations/eat/README.md b/integrations/eat/README.md index 20711d19..f6625388 100644 --- a/integrations/eat/README.md +++ b/integrations/eat/README.md @@ -4,7 +4,9 @@ Runs Intel's [Enterprise Agent Toolkit](https://github.com/intel/enterprise-agen (EAT) `release-1.0.3-hotfix` (commit `0d3438d`) on one k3d-gpu node, on a client CPU with no AVX-512 and no AMX. EAT's docs target Xeon clusters built by Kubespray. Here it goes through EAT's own **brownfield** path ("Vanilla -Kubernetes", an existing kubeconfig). No EAT file is edited. +Kubernetes", an existing kubeconfig). No EAT file is edited. Intel's latest +release, `release-1.1.0` (commit `0c726f5`), deploys with +`EAT_RELEASE=release-1.1.0`: see [EAT releases](#eat-releases). Verified on a Lenovo P360 Ultra: i9-13900T (8 P-cores = CPUs 0-15, 16 E-cores = CPUs 16-31, AVX2 + AVX-VNNI only), 125 GB RAM, Docker 29.3, k3d 5.9.0, k3s @@ -56,11 +58,13 @@ that off the host. | File | Role | |------|------| -| `eat-up.sh` | clone + bastion build + run; `model ` deploys one catalog model | +| `eat-up.sh` | release check (`EAT_RELEASE`) and config checks + clone + bastion build + run; `model ` deploys one catalog model | | `brownfield.sh` | runs in the bastion; answers EAT's prompts and calls its brownfield functions | | `bastion.Dockerfile` | Ubuntu 24.04 (digest-pinned) plus what EAT's one-click installer installs | -| `agentic-config.cfg` | EAT's own component toggles: gateway, ingress, Redis, pgvector, agent sandbox on; Kubespray, observability, models, KubeRay, CPU balloons off | +| `agentic-config.cfg` | EAT's own component toggles: gateway, ingress, Redis, pgvector, agent sandbox on; Kubespray, observability, models, KubeRay, CPU balloons, guardrail off | | `vllm-values.yaml` | k3d-gpu values merged over EAT's `vllm/xeon-values.yaml` (memory limit, P-core balloon, image digest) | +| `vllm-image-release-1.1.0.yaml` | `release-1.1.0`'s vLLM CPU image, `v0.28.0` by digest; merged last, for catalog models and the guard | +| `vllm-merge.py` | merges the overlays over EAT's vLLM values and refuses a vLLM tag other than the one EAT's file names | | `gateway-charts.py` | writes pinned copies of EAT's GenAI Gateway chart directories; `brownfield.sh` passes them to `deploy-genai-gateway.yml` | | `gateway-values.yaml` | the gateway chart's pins: postgresql and redis chart versions, LiteLLM, PostgreSQL, Redis and init images by digest | | `gateway-trace-values.yaml` | the Langfuse trace stack's pins: Langfuse web and worker, PostgreSQL, Valkey, ClickHouse, ZooKeeper, MinIO by digest | @@ -73,6 +77,67 @@ The gateway is reached on the host IP of the running ingress-nginx controller pod (the k3d node IP). ingress-nginx binds hostPort 80/443 there, with `Host: api.example.com` (`cluster_url` in `agentic-config.cfg`) over TLS with a self-signed cert. The host's own ports 80/443 are not used. +## EAT releases + +`EAT_RELEASE` picks the release `eat-up.sh` clones. Each is pinned to its commit, and +an `EAT_WORK` cloned for one release is refused for the other (use a fresh +`EAT_WORK` per release). Any other value is refused before the config is read. + +| `EAT_RELEASE` | Commit | vLLM CPU image for catalog models | +|---|---|---| +| `release-1.0.3-hotfix` (default) | `0d3438d` | `v0.24.0` by digest, from `vllm-values.yaml` | +| `release-1.1.0` | `0c726f5` | `v0.28.0` by digest, from `vllm-image-release-1.1.0.yaml` | + +```bash +EAT_RELEASE=release-1.1.0 EAT_WORK=$PWD/eat-work-1.1.0 integrations/eat/eat-up.sh +``` + +`release-1.1.0` (2026-10-22) is Intel's latest release; GitHub now flags +`release-1.0.3-hotfix` as a prerelease. The default stays `release-1.0.3-hotfix` +until a fresh run on 1.1.0 passes: EXPERIMENTS.md H1c (pending) repeats H1b's run +with `EAT_RELEASE=release-1.1.0`. Nothing has run against a 1.1.0 stack through +`eat-up.sh` yet, so everything below about 1.1.0 is read from its source and +checked with stubs in `tests/eat_test.sh`. + +`eat-up.sh` checks `agentic-config.cfg` against the release before it touches the +cluster. It parses each key as EAT's `read_config_file` does (trimmed, quotes +dropped, comments skipped, the last assignment wins): + +| Key | `release-1.0.3-hotfix` | `release-1.1.0` | Why | +|---|---|---|---| +| `deploy_nri_balloon_policy` anything but `off` or `no`, or absent | refused | refused | EAT's policy would be a second one beside k3d-gpu's (blocker 10). Both releases turn it on when the key is absent | +| `deploy_openshell=on` | refused | passed to EAT | 1.0.3 has no OpenShell role | +| `deploy_guardrails=on` | refused | passed to EAT, guard image by digest | 1.0.3 has no guardrail role | +| `compute_platform=xpu` (or `x`) | not checked | refused | EAT applies intel-device-plugins' GPU plugin from `main`, unpinned, beside k3d-gpu's own. For vLLM on an Intel GPU: `K3D_GPU_AGENT_BACKEND=vllm k3d-gpu agent up` | +| `deploy_kata_containers=on` | not checked | refused | Kata microVMs need `/dev/kvm`; k3d nodes have none | +| `rack_scale_mode=on` | not checked | refused | builds its own cluster from `rack-profile.yaml` (netplan, BGP, the NVIDIA GPU operator) | + +**Guardrail.** `release-1.1.0`'s `deploy-agentic-stack.sh` sets +`DEPLOY_GUARDRAILS="${DEPLOY_GUARDRAILS:-on}"` at top level (line 195). That line runs +when `brownfield.sh` sources the script; the `deploy_guardrails` key is read only in +its `main()` (lines 1136-1140), which a sourced run skips. So `brownfield.sh` exports +`DEPLOY_GUARDRAILS` from the key before it sources the script: `on` for `on` or `yes`, +else `off`. The shipped config sets `deploy_guardrails=off`. With `deploy_guardrails=on`, +`deploy-guardrails.yml` installs the guard (`Qwen/Qwen3Guard-Gen-0.6B`, release +`vllm-guard` in namespace `guardrails`) from `{{ remote_helm_charts_base }}/vllm` +with `guardrail-values.yaml`, which names `vllm/vllm-openai-cpu:v0.28.0` by tag only. +`brownfield.sh` copies EAT's `vllm` chart to `~/k3d-gpu-guard-charts` in the bastion, +writes the overlay's digest into that copy's `guardrail-values.yaml` +(`vllm-merge.py --require-digest`), and passes that one playbook +`remote_helm_charts_base` as an extra var. In `deploy-guardrails.yml` the variable +serves only the guard's chart and values (lines 115, 133, 135). Everything else in +the guard's values stays EAT's: 2 CPUs, 16Gi, a 4 GiB KV cache, and no balloon +annotation. A marker file fails the deploy if the playbook ran without the copy. + +**vLLM tag check.** `brownfield.sh model ` merges EAT's `xeon-values.yaml`, then +`vllm-values.yaml`, then the release's image overlay (`vllm-merge.py`). The merge is +refused when the merged tag, up to the `@`, is not the one EAT's own file names. EAT +writes its model configs for the vLLM it ships. Gemma 4 on v0.24.0 stalls in vLLM's +CPU split-KV attention (EXPERIMENTS.md, crescent amendment E1); v0.28.0 contains the +fix, [vllm#45844](https://github.com/vllm-project/vllm/pull/45844) (`26eb8720`). For +`release-1.0.3-hotfix` the merged values are byte for byte what `brownfield.sh` wrote +before the check. + ## Observed end state `eat-check.sh` on the live cluster: @@ -140,14 +205,15 @@ the one behind `agent up --model`) to the catalog model, as values only. `eat-opt.py` runs in the bastion after the values merge. It reads the Hugging Face id from the `--set LLM_MODEL_ID="..."` line of EAT's playbook block for that catalog id. It asks the optimizer what that model wants on vLLM on the CPU, at the -chart's vLLM version (the image tag, `v0.24.0`). Then it edits that model's -`modelConfigs` entry in the merged values file: +chart's vLLM version (the merged image tag: `v0.24.0`, or `v0.28.0` on +`release-1.1.0`). Then it edits that model's `modelConfigs` entry in the merged +values file: - engine arguments the entry lacks are appended. EAT's own arguments are never removed or changed: a flag EAT already sets is kept and reported. - a speculative config is set (with `speculative.enabled`) when the optimizer - chose one. vLLM v0.24.0 marks speculative decoding unsupported on the CPU, so - none is chosen today. + chose one. vLLM marks speculative decoding unsupported on the CPU before + v0.31.0, so none is chosen on either release's image. - the model id stays EAT's. The playbook passes it with `--set`, which wins over any values file, so a different variant is reported as not applied. @@ -250,10 +316,11 @@ the balloon path, which is off here. ## Versions: pinned and floating -Pinned here: EAT by tag and commit (`eat-up.sh` refuses any other HEAD), the +Pinned here: EAT by release and commit (`eat-up.sh` refuses any other HEAD), the bastion base image by digest, Helm v3.14.0 baked into the bastion by sha256, -the vLLM image by digest (`vllm-values.yaml`), and the GenAI Gateway's charts -and images. EAT itself pins ingress-nginx 4.12.2 (`ingress_controller` in +the vLLM image by digest (`vllm-values.yaml`; on `release-1.1.0` +`vllm-image-release-1.1.0.yaml`, which also pins the guard), and the GenAI +Gateway's charts and images. EAT itself pins ingress-nginx 4.12.2 (`ingress_controller` in `agentic-metadata.cfg`), kubectl v1.29.0, agent-sandbox v0.5.0 and the Langfuse chart 1.5.30. `images.lock` lists every image pinned here, and `scripts/images-lock pins` fails CI on any image in these files without a digest. @@ -310,9 +377,11 @@ observed. (brownfield_deployment.sh:193). k3s's `local-path` class serves every PVC instead. The vLLM chart pins `storageClassName: local-path`. - **EAT's CPU balloons policy** (`deploy_nri_balloon_policy`), see blocker 10. - On a k3d-gpu node without `hybrid` it could install. That case is - UNVERIFIED (probe: `K3D_GPU_VENDORS=nvidia,intel k3d-gpu up`, then - `deploy_nri_balloon_policy=on`). + `eat-up.sh` refuses any value but `off` or `no`, on both releases. On a + k3d-gpu node without `hybrid` it could install; that case is UNVERIFIED and + needs EAT's own path, not `eat-up.sh`. +- **On `release-1.1.0`: the XPU path, Kata and rack-scale mode.** `eat-up.sh` + refuses each (see [EAT releases](#eat-releases)). - **Catalog models above the host's RAM.** EAT's default `cpu-qwen2-5-coder-14b` needs about 28 GiB of bf16 weights plus KV cache. Not tried here.