reap

module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: Apache-2.0

README

reap

CI Release License

Find and cut wasted GPU spend in Kubernetes. Static checks for ML/GPU workloads, from your CI to your cluster.

reap scans Kubernetes manifests for GPU waste and ML workload misconfigurations — idle GPUs, bad scheduling, missing production-readiness — and reports them in your terminal and in CI, before they cost you money. Think "kube-linter, specialized for GPU and ML workloads."

It reads local files only: no network, no credentials, no cluster access, no telemetry.

Who is it for

  • ML engineers writing a PyTorchJob or a vLLM deployment, who want to know before deploying what will be rejected, deadlock, or crash with cryptic shared-memory errors.
  • DevOps / platform engineers who want a CI guardrail so GPU manifests that waste money or break production never merge.
  • Anyone paying the GPU bill who wants "job hung since Thursday" and "notebook idle all weekend" caught by a linter, not by the invoice.

Where it fits:

flowchart LR
    M["manifests/*.yaml"] --> R["reap"]
    H["helm template / kustomize build"] -- stdin --> R
    B[".reap-baseline.json<br>(accepted findings)"] -. suppresses .-> R
    R --> T["terminal report<br>with fix hints"]
    R --> J["JSON + stable<br>fingerprints"]
    R --> E{"exit code"}
    E -- "0" --> OK["CI passes"]
    E -- "1" --> KO["CI fails:<br>new findings"]

Install

Prebuilt binaries for Linux, macOS, and Windows (amd64/arm64) are on the Releases page. Or:

# with Go installed
go install github.com/emphity1/reap/cmd/reap@latest

# from source
git clone https://github.com/emphity1/reap.git && cd reap && make build

# as a Docker image (distroless, ~5 MB, amd64/arm64)
docker run --rm -v "$PWD:/work:ro" ghcr.io/emphity1/reap:v0.2.2 /work
helm template . | docker run --rm -i ghcr.io/emphity1/reap:v0.2.2 -

Images are published to GHCR on every release (:vX.Y.Z and :latest); make docker still builds a local reap:dev from source.

Usage

# lint a file or a directory (scanned recursively for *.yaml / *.yml)
reap ./manifests/

# lint rendered charts / overlays via stdin
helm template . | reap -
kustomize build . | reap -

Example output (severity-colored on a terminal; -color auto|always|never, NO_COLOR respected):

manifests/app.yaml
  Deployment/ml/llm-inference
    [error] no-gpu-limit
        container "server" requests nvidia.com/gpu: 1 but sets no limit; Kubernetes rejects GPU
        requests without an equal limit, so this manifest will not deploy
        fix: set resources.limits["nvidia.com/gpu"] equal to the request

reap: 2 objects checked, 1 finding (1 error, 0 warning, 0 info)

Findings are grouped by file and object, ordered by severity (errors first), and every finding carries a concrete fix.

Exit codes
Code Meaning
0 no findings at/above the threshold
1 findings at/above the threshold (CI should fail)
2 usage or input error

The threshold is set with -fail-on (info, warning, error, or none to always exit 0); the default is warning.

JSON output

-format json emits a machine-readable report (schema version 1):

{
  "version": "1",
  "summary": {
    "objectsChecked": 2,
    "findings": 1,
    "bySeverity": {"error": 1, "warning": 0, "info": 0},
    "suppressed": 0,
    "staleBaselineEntries": 0
  },
  "findings": [
    {
      "ruleId": "no-gpu-limit",
      "severity": "error",
      "message": "container \"server\" requests nvidia.com/gpu: 1 but sets no limit; ...",
      "object": "Deployment/ml/llm-inference",
      "detail": "server/nvidia.com/gpu",
      "source": "manifests/app.yaml",
      "fix": "set resources.limits[\"nvidia.com/gpu\"] equal to the request",
      "fingerprint": "c3c88f7fa45669b8"
    }
  ]
}

fingerprint is the finding's stable identity: a hash of the rule ID, the object reference (Kind/Namespace/Name), and a semantic detail key (container and resource names — never positional indices). Message wording, severity, suggested fix, and file path are metadata and never enter the hash, so the same chart produces the same fingerprint whether linted from a file or piped through stdin, and fingerprints survive copy edits and file moves. Identical logical findings from different files (e.g. two overlays defining the same object with the same violation) deliberately share one fingerprint. The baseline mechanism is a set of these fingerprints.

Adopting reap on an existing repo (baseline)

Existing repos have existing findings. Accept them once, then gate CI only on new problems:

reap -write-baseline .reap-baseline.json ./manifests/   # day one: accept the past
reap -baseline .reap-baseline.json ./manifests/         # from then on: only new findings fail

The baseline file is JSON — fingerprints plus human-readable context and an optional reason field per entry; commit it to the repo. Suppressed findings are hidden from the output and the exit-code gate, and the summary reports both how many were suppressed and how many baseline entries went stale (matched nothing — fixed for real, so prune them). -baseline and -write-baseline are mutually exclusive; to refresh a baseline, run -write-baseline again.

CI

GitHub Actions — this repo doubles as an action:

- uses: emphity1/reap@v0.2.2
  with:
    path: ./manifests/
    fail-on: warning
    # baseline: .reap-baseline.json

GitLab CI — copy the job from docs/ci/gitlab-ci.yml.

Releases are built by GoReleaser on version tags (v*): prebuilt binaries for Linux, macOS, and Windows (amd64/arm64) appear on the Releases page.

Rules

Rules see pod templates wherever manifests put them: the core workload kinds (Pod, Deployment, StatefulSet, DaemonSet, Job, CronJob, ...) plus CRDs that embed templates in nested maps or arrays — Kubeflow's PyTorchJob/TFJob/MPIJob, RayCluster and RayJob worker groups, Volcano Job tasks. The rule set is dogfooded against real vendor charts (KubeRay, NVIDIA GPU Operator, JupyterHub, vLLM production-stack, KServe, Triton, Kueue, Volcano, ...) and upstream tutorial manifests; the reproducible harness and its findings live in hack/dogfood/.

ID Severity Checks
no-gpu-limit error a container requests a GPU without an equal limit (rejected by the API server; the request/limit pair must be equal for extended resources)
job-no-deadline warning a GPU batch/v1 Job or CronJob sets no activeDeadlineSeconds, so a hung run holds its GPUs indefinitely (same-named CRD kinds, like Volcano's Job, are recognized and skipped — their schema differs)
model-server-no-probes warning a known inference server (vLLM, Triton, TGI, TorchServe, SGLang, Ollama, the KServe serving runtimes, NIM, LMDeploy) has no readinessProbe, so traffic arrives minutes before the model finishes loading
distributed-training-no-gang-scheduling warning a multi-replica GPU training job (PyTorchJob, TFJob, MPIJob, XGBoostJob, PaddleJob) has no visible gang-scheduling config (runPolicy.schedulingPolicy, Kueue/KAI/YuniKorn queue labels, Volcano/coscheduling scheduler or annotations) — replicas can deadlock holding GPUs
shm-too-small warning a GPU container has no memory-backed /dev/shm (emptyDir with medium: Memory); the 64 MB default makes shared-memory users — PyTorch DataLoader workers, NCCL, Triton's Python backend — crash or stall with cryptic errors. Deliberately broad: the fix is cheap and harmless; runtimes that never touch shared memory should baseline it. Ray CRDs are excluded — the KubeRay operator injects the mount itself
gpu-no-node-targeting info a GPU workload sets no nodeSelector, node affinity, or tolerations; on clusters with tainted GPU nodes it sits Pending and the autoscaler won't scale the GPU pool for it
multinode-no-topology-affinity info a multi-replica GPU training job expresses no placement intent (affinity, topologySpreadConstraints, nodeSelector, or topology-aware scheduler hints) — NCCL collectives fall back to the slow network path
notebook-no-idle-culling info a GPU notebook (Jupyter image or Kubeflow Notebook) has no visible idle-culling configuration — a forgotten notebook holds its GPU all weekend
inference-no-hpa info a model-server Deployment/StatefulSet has no HPA targeting it in the linted input — fixed replicas idle GPUs off-peak or throttle at peak
no-pdb info a model server has no PodDisruptionBudget selecting its pods in the linted input (matchLabels matching; matchExpressions PDBs are assumed to match)

Every rule ships with passing and failing fixtures in testdata/. Severity is a discipline: error means the manifest is broken, warning means money or uptime is at risk, info marks heuristics and cross-object checks whose evidence may legitimately live outside the linted files — those explain themselves and lean on the baseline.

Deliberately not checked, because a manifest cannot answer it: actual GPU utilization (a runtime measurement — needs DCGM/metrics, a later phase) and node-pool scale-to-zero (lives in Terraform or autoscaler CRDs, not in workload manifests). A linter that guesses burns trust; these return only when they can be answered honestly.

The same honesty applies to operator-managed serving and training CRDs (KServe InferenceService, Kubeflow Trainer v2 TrainJob): their pod construction belongs to the operator or a referenced runtime, not to the manifest. KServe injects probes and autoscales on its own, and a TrainJob carries no pod template at all — so pod-level findings there would be unfixable or plain wrong. reap lints what you control; workloads those operators generate are linted where they are defined (e.g. a ClusterTrainingRuntime rendered to YAML pipes into reap - like anything else).

Development

make build   # build bin/reap
make test    # go test ./...
make lint    # gofmt + go vet

See CONTRIBUTING.md. Licensed under Apache 2.0.

Directories

Path Synopsis
cmd
reap command
Command reap lints Kubernetes manifests for GPU waste and ML workload misconfigurations.
Command reap lints Kubernetes manifests for GPU waste and ML workload misconfigurations.
hack
dogfood/stats command
Command stats prints corpus statistics for the dogfood run: object, pod-spec, container, and GPU-requesting-container counts per input file.
Command stats prints corpus statistics for the dogfood run: object, pod-spec, container, and GPU-requesting-container counts per input file.
internal
baseline
Package baseline suppresses known findings so reap can be adopted on existing repos without failing CI on pre-existing problems.
Package baseline suppresses known findings so reap can be adopted on existing repos without failing CI on pre-existing problems.
engine
Package engine runs every rule over every object and collects findings.
Package engine runs every rule over every object and collects findings.
parser
Package parser reads Kubernetes YAML manifests into normalized objects.
Package parser reads Kubernetes YAML manifests into normalized objects.
report
Package report renders findings for humans and machines.
Package report renders findings for humans and machines.
rules
Package rules defines the lint rules and the types they share.
Package rules defines the lint rules and the types they share.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL