Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions argo/homelab-media-gpu.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: homelab-media-gpu-
namespace: argo
labels:
app.kubernetes.io/component: homelab-media-gpu
app.kubernetes.io/part-of: bluefin-test-suite
spec:
workflowTemplateRef:
name: homelab-media-gpu
236 changes: 236 additions & 0 deletions argo/workflow-templates/homelab-media-gpu.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
name: homelab-media-gpu
namespace: argo
labels:
app.kubernetes.io/component: homelab-media-gpu
app.kubernetes.io/part-of: bluefin-test-suite
spec:
entrypoint: pipeline
onExit: cleanup
serviceAccountName: homelab-runner
arguments:
parameters:
- name: lane
value: "homelab-media-gpu"
- name: branch
value: "main"
- name: gpu-resource-key
value: "nvidia.com/gpu"
- name: device-plugin-label
value: "app=nvidia-device-plugin"
templates:
- name: pipeline
dag:
tasks:
- name: check-gpu-prerequisites
template: check-gpu-prerequisites
arguments:
parameters:
- name: gpu-resource-key
value: "{{workflow.parameters.gpu-resource-key}}"
- name: device-plugin-label
value: "{{workflow.parameters.device-plugin-label}}"
- name: create-namespace
depends: "check-gpu-prerequisites.Succeeded"
template: create-namespace
arguments:
parameters:
- name: namespace
value: "homelab-media-gpu-{{workflow.uid}}"
- name: deploy-fixture
depends: "create-namespace.Succeeded"
template: deploy-fixture
arguments:
parameters:
- name: namespace
value: "homelab-media-gpu-{{workflow.uid}}"
- name: lane
value: "{{workflow.parameters.lane}}"
- name: gpu-resource-key
value: "{{workflow.parameters.gpu-resource-key}}"
- name: run-tests
depends: "deploy-fixture.Succeeded"
templateRef:
name: run-incluster-tests
template: run-pytest
arguments:
parameters:
- name: namespace
value: "homelab-media-gpu-{{workflow.uid}}"
- name: suite-path
value: "service_catalog/media_gpu"
- name: lane
value: "{{workflow.parameters.lane}}"
- name: app-label
value: "app=homelab-media-gpu"
- name: service-name
value: "homelab-media-gpu"
- name: branch
value: "{{workflow.parameters.branch}}"

- name: check-gpu-prerequisites
# Guard step: verify GPU device plugin is available before allocating
# cluster resources. Fails the workflow early with a clear message
# rather than creating a namespace and leaving a pending pod.
inputs:
parameters:
- name: gpu-resource-key
- name: device-plugin-label
script:
image: cgr.dev/chainguard/kubectl:latest-dev
command: [bash, -c]
source: |
set -euo pipefail
GPU_KEY="{{inputs.parameters.gpu-resource-key}}"
PLUGIN_LABEL="{{inputs.parameters.device-plugin-label}}"

echo "=== Checking GPU allocatable capacity ===" >&2
ALLOCATABLE=$(kubectl get nodes -o json | \
python3 -c "
import sys, json
nodes = json.load(sys.stdin).get('items', [])
total = 0
for n in nodes:
val = n.get('status', {}).get('allocatable', {}).get('${GPU_KEY}', '0')
try: total += int(val)
except: pass
print(total)
")
echo "Allocatable ${GPU_KEY}: ${ALLOCATABLE}" >&2

if [ "${ALLOCATABLE}" -lt 1 ]; then
echo "ERROR: No allocatable ${GPU_KEY} found on any node." >&2
echo "GPU transcoding lane requires substrate work from #54:" >&2
echo " 1. Install NVIDIA driver on host node (ghost)" >&2
echo " 2. Install nvidia-container-toolkit" >&2
echo " 3. Deploy nvidia-device-plugin DaemonSet" >&2
echo " 4. Verify: kubectl get nodes -o json | grep -i gpu" >&2
exit 1
fi

echo "=== Checking device plugin DaemonSet ===" >&2
RUNNING=$(kubectl get pods --all-namespaces -l "${PLUGIN_LABEL}" \
--field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l)
echo "Running device plugin pods: ${RUNNING}" >&2

if [ "${RUNNING}" -lt 1 ]; then
echo "ERROR: No Running pods found for label '${PLUGIN_LABEL}'." >&2
echo "Install and verify the nvidia-device-plugin DaemonSet (see #54)." >&2
exit 1
fi

echo "GPU prerequisites satisfied." >&2

- name: create-namespace
inputs:
parameters:
- name: namespace
resource:
action: create
manifest: |
apiVersion: v1
kind: Namespace
metadata:
name: "{{inputs.parameters.namespace}}"
labels:
app.kubernetes.io/part-of: bluefin-test-suite
bluefin.io/lane: homelab-media-gpu

- name: deploy-fixture
inputs:
parameters:
- name: namespace
- name: lane
- name: gpu-resource-key
script:
image: cgr.dev/chainguard/kubectl:latest-dev
command: [bash, -c]
source: |
set -euo pipefail
NS="{{inputs.parameters.namespace}}"
GPU_KEY="{{inputs.parameters.gpu-resource-key}}"
kubectl apply -n "${NS}" -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: homelab-media-gpu-config
labels:
app: homelab-media-gpu
app.kubernetes.io/part-of: bluefin-test-suite
bluefin.io/lane: "{{inputs.parameters.lane}}"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: homelab-media-gpu
labels:
app: homelab-media-gpu
app.kubernetes.io/part-of: bluefin-test-suite
bluefin.io/lane: "{{inputs.parameters.lane}}"
bluefin.io/workload-class: media-gpu
spec:
replicas: 1
selector:
matchLabels:
app: homelab-media-gpu
template:
metadata:
labels:
app: homelab-media-gpu
app.kubernetes.io/part-of: bluefin-test-suite
bluefin.io/lane: "{{inputs.parameters.lane}}"
bluefin.io/workload-class: media-gpu
spec:
nodeSelector:
kubernetes.io/hostname: ghost
runtimeClassName: nvidia
containers:
- name: media-transcoder
# NVIDIA CUDA base image with ffmpeg for hardware transcoding
# validation. Includes nvidia-smi, CUDA libraries, and
# ffmpeg built with NVENC/NVDEC support.
# Source: https://hub.docker.com/r/linuxserver/ffmpeg
image: docker.io/linuxserver/ffmpeg:latest
command: [sh, -c, "sleep 3600"]
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: "UTC"
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
- name: NVIDIA_DRIVER_CAPABILITIES
value: "compute,video,utility"
resources:
limits:
"${GPU_KEY}": "1"
volumeMounts:
- name: config
mountPath: /config
volumes:
- name: config
persistentVolumeClaim:
claimName: homelab-media-gpu-config
EOF
kubectl rollout status deployment/homelab-media-gpu -n "${NS}" --timeout=300s >&2

- name: cleanup
script:
image: cgr.dev/chainguard/kubectl:latest-dev
command: [bash, -c]
source: |
set -euo pipefail
NS="homelab-media-gpu-{{workflow.uid}}"
kubectl delete namespace "${NS}" --ignore-not-found=true >&2 || true
timeout 180 bash -c "until ! kubectl get namespace \"${NS}\" >/dev/null 2>&1; do sleep 5; done" >&2 || true
echo "cleanup-complete"
71 changes: 70 additions & 1 deletion docs/homelab-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,77 @@ The lab validates the following hostname/routing pattern for exposed in-cluster
| Issue | Status | Dependency |
|---|---|---|
| #62 RWX / shared-storage | ❌ blocked | NFS CSI or Longhorn installation on ghost |
| #63 GPU transcoding lane | ❌ deferred | GPU passthrough KubeVirt feature gate |
| #63 GPU transcoding lane | ✅ defined | Substrate work from #54 required before tests execute |
| #61 auth-gated service UI | ❌ deferred | service-catalog baseline lane first |
| #60 first restore drill | ✅ implemented | `homelab-restore-drill` WorkflowTemplate + `tests/homelab_backup/` |
| #84 PVC restore drill with backup artifact | ✅ implemented | `homelab-restore-drill` WorkflowTemplate + `tests/homelab_backup/` |
| Media service lane | ❌ deferred | #62 (shared mount) + #63 (GPU) |

---

## 7. GPU Transcoding and Hardware-Passthrough Lane (#63)

Split from the base media-service lane (#59). Defines the in-cluster
validation contract for NVIDIA CUDA/NVENC hardware-accelerated transcoding.
AMD ROCm and Intel QSV paths are deferred; KubeVirt VM-backed passthrough
is tracked in #54.

### Substrate dependency chain

```
#54 substrate (GPU device plugin + driver stack on ghost)
→ #63 this lane (GPU transcoding contract proven)
→ #59 base media lane (unblocked from GPU split)
```

### Required substrate from #54 before this lane executes

| Requirement | How to verify |
|---|---|
| NVIDIA driver loaded on host (ghost) | `nvidia-smi` on host returns GPU name |
| nvidia-container-toolkit installed | `nvidia-ctk --version` succeeds |
| nvidia-device-plugin DaemonSet running | `kubectl get pods -A -l app=nvidia-device-plugin` shows Running |
| Node reports allocatable GPU capacity | `kubectl get nodes -o json` has `nvidia.com/gpu` > 0 in allocatable |
| Container runtimeClass `nvidia` registered | `kubectl get runtimeclass nvidia` exists |

### Behavior table (tests run only when GPU is allocatable)

| Behavior | Test | Artifact |
|---|---|---|
| Node has allocatable GPU capacity | `test_gpu_node_has_allocatable_capacity` | `gpu-node-allocatable.txt` |
| Device plugin DaemonSet is Running | `test_device_plugin_daemonset_is_running` | `gpu-device-plugin-pods.json` |
| GPU Deployment reaches `availableReplicas >= 1` | `test_gpu_deployment_becomes_ready` | `gpu-deployment.json` |
| Pod with GPU resource limit reaches Running | `test_gpu_pod_reaches_running_state` | `gpu-pods.json` |
| GPU resource limit present in pod spec | `test_gpu_resource_limit_present_in_pod_spec` | — |
| /dev/nvidia* device node visible in container | `test_gpu_device_node_visible_in_container` | `gpu-dev-nodes.txt` |
| nvidia-smi reports GPU in container | `test_nvidia_smi_reports_gpu_in_container` | `gpu-nvidia-smi.txt` |
| ffmpeg lists nvenc encoder | `test_ffmpeg_lists_nvenc_encoder` | `gpu-ffmpeg-encoders.txt` |
| ffmpeg hardware transcode completes (1-second clip) | `test_ffmpeg_hardware_transcode_completes` | `gpu-transcode-output.txt` |
| GPU capacity recovers after pod deletion | `test_gpu_resource_is_released_after_pod_deletion` | `gpu-allocatable-after-delete.txt` |
| KubeVirt VM-backed passthrough | `test_kubevirt_vm_gpu_passthrough_is_out_of_scope_for_this_lane` | `pytest.skip` → #54 |
| Multi-GPU / MIG slicing | `test_multi_gpu_and_mig_slicing_is_out_of_scope_for_this_lane` | `pytest.skip` → deferred |
| AMD ROCm / Intel QSV | `test_amd_and_intel_gpu_paths_are_out_of_scope_for_this_lane` | `pytest.skip` → deferred |

### Module-level skip gate

All tests are gated at module import time by `_gpu_allocatable_on_any_node()`.
If no `nvidia.com/gpu` (or `TEST_GPU_RESOURCE_KEY`) resource appears in any
node's allocatable map, the entire module is skipped with a message pointing
to the #54 substrate requirements above. This means the GPU lane can be
included in standard CI without failing on non-GPU clusters.

### WorkflowTemplate guard step

`check-gpu-prerequisites` runs before namespace creation. If it fails
(no allocatable GPU, no Running device plugin pods), the workflow exits
with a clear error message listing the three steps needed from #54. No
cluster resources are created for a non-GPU cluster.

### Out-of-scope splits

| Path | Reason | Tracked |
|---|---|---|
| KubeVirt VM-backed GPU passthrough (VFIO/IOMMU) | Requires additional substrate + #54 | #54 |
| Multi-GPU scheduling / MIG slicing | No multi-GPU hardware in current lab | Deferred |
| AMD ROCm (amd.com/gpu) | Primary target is NVIDIA | Deferred |
| Intel QSV / VAAPI (i915/xe) | Deferred until NVIDIA path validated | Deferred |
Empty file.
Loading