GitHub

Keep a workload correct and available when its preferred compute is unobtainable—by descending an ordered ladder of substitution rungs until one is obtainable, and climbing back up when the preferred rung frees.

The descent is only half the pattern. The climb-back (promotion) is what makes this a supply pattern rather than a one-way degradation slide: when the preferred rung's capacity returns, the workload migrates back up under policy. A "ladder" reads as purely downward—resist that framing; the promotion arm is load-bearing.

This is a small, vendor-neutral, dependency-free reference implementation—a teaching artifact you can read in one sitting and pip install, not a framework. It is not a scheduler, a control plane, or a Kubernetes operator. Cloud products appear here only as examples of the pattern, never as dependencies of the core.

This is a reference implementation. It ships no benchmarks, claims no production adoption, and invents no performance numbers. Its job is to make the pattern concrete in code.

Why this exists: Compute Scarcity Is Permanent. Build a Ladder. makes the case for the pattern. This repo is the runnable companion. Read the post for the argument; read the code here to use it.

The pattern in 20 lines

from compute_fallback_ladder import (
    Ladder, Policy, Requirement, Rung, Selector, InMemoryProbe,
)
ladder = Ladder(rungs=[
    Rung("preferred", Requirement("H100", 8, 640), cost=100.0, quality=1.0),
    Rung("alternate", Requirement("A100", 8, 320), cost=60.0,  quality=0.9),
    Rung("cpu",       Requirement(None,  0, 64),   cost=5.0,   quality=0.4),  # floor: always serves
], policy=Policy(promotion_hysteresis=2))
probe = InMemoryProbe(available={"H100": 8, "A100": 8})
selector = Selector(ladder, probe, on_transition=print)
selector.select()                 # -> preferred (highest obtainable now)
probe.set_available("H100", 0)    # preferred capacity withdrawn
selector.select()                 # -> alternate (immediate descent)
probe.set_available("H100", 8)    # capacity returns...
selector.select(); selector.select()  # -> preferred (promoted after hysteresis clears)

Run the 30-second demo without writing any code:

$ python -m examples.local.demo

The core abstraction

Concept What it is
Rung One substitution tier: what it needs (Requirement: accelerator kind / count / memory), what it costs, what quality it delivers.
Ladder The ordered rungs (most preferred first) plus a Policy. The floor rung must be CPU-only so it is always obtainable.
Selector Picks the highest obtainable rung right now. Descent is immediate; promotion is gated by hysteresis. This is the heart of the pattern.
CapacityProbe The pluggable seam: "can I actually get this right now?" Ships an InMemoryProbe fake and a real, vendor-neutral KubernetesProbe.
Promotion The climb-back, built into the Selector under Policy.
Transition The observability event emitted on every change: which rung is active, why, and every transition—including a max_hold_exceeded alert when degradation persists too long.

Install

$ pip install compute-fallback-ladder      # core: zero runtime dependencies
$ pip install 'compute-fallback-ladder[ml]' # optional: the real-model ml example

Requires Python 3.12+.

The pattern, shipping: the GKE ComputeClass CRD

The whole pattern already exists as a product primitive. A GKE custom ComputeClass is a ladder you declare instead of build—the ordered rungs, the floor behavior, and the climb-back are all fields:

apiVersion: cloud.google.com/v1
kind: ComputeClass
metadata:
  name: fallback-ladder-example
spec:
  priorities: # THE LADDER: ordered rungs, tried top to bottom
    - machineType: a3-highgpu-8g # rung 0: 8x H100, the preferred rung
    - machineType: a2-highgpu-8g # rung 1: 8x A100
    - machineType: g2-standard-48 # rung 2: L4
    - machineFamily: n4 # floor: CPU-only, broadly available
  whenUnsatisfiable: ScaleUpAnyway # THE FLOOR: descend rather than place nothing
  nodePoolAutoCreation:
    enabled: true # let the autoscaler build a pool for the rung it lands on
  activeMigration:
    optimizeRulePriority: true # PROMOTION: climb back when a better rung frees

Three fields carry the pattern, and two of them are traps if you leave them out:

Field Pattern role Default if omitted
priorities The ladder itself—the selector's ordered rungs (required)
whenUnsatisfiable Floor behavior DoNotScaleUp—Pod stays Pending, class reports CrdMisconfigured
nodePoolAutoCreation Whether GKE may build a pool to reach a rung false—only pre-existing pools match
activeMigration Promotion, the climb-back arm off—descent becomes one-way

Omitting whenUnsatisfiable inverts the pattern: the floor becomes "serve nothing." Omitting activeMigration deletes half of it: you get a slide, not a ladder.

This was verified on a live GKE cluster, not asserted. Both failure modes above were observed before they were documented, and a full climb-back—provision the higher rung, cordon, drain, migrate the Pod up, delete the drained node—took 5m41s end to end. The session logs and the exact commands are in examples/gke-computeclasses/VERIFIED.md.

The honest caveat, carried verbatim from the GKE docs: "Custom ComputeClasses influence autoscaling decisions but are not considered by kube-scheduler." This is the autoscaling arm of the ladder, not a per-Pod scheduling guarantee.

Google Cloud appears here as an example of the pattern shipping in a real product, never as a dependency of the core library—which stays vendor-neutral and dependency-free.

Examples

Each example actually runs, and the local + kubernetes examples are exercised end to end in CI.

  • examples/local/—pure-fake probe, zero cloud. Watch the ladder walk down as capacity is withdrawn and climb back when it returns.
  • examples/kubernetes/—the real KubernetesProbe against a kind/minikube cluster. Vendor-neutral, reproducible on a laptop.
  • examples/gke-computeclasses/—the worked, fully annotated manifest behind the section above, plus the live verification record.
  • examples/ml_inference/—the motivating case: preferred accelerator → alternate → quantized/smaller model → CPU → cached answer, with the quality-loss trap demonstrated on a genuinely small open model.

Cloud Mechanism Reference

The pattern is cloud-agnostic, and the post argues why in The Mechanisms Already Ship. This table is the reference you come back to: each cloud already provides mechanisms you wire into a CapacityProbe and a rung ladder, linked to its primary documentation.

Pattern concept Google Cloud AWS Azure
Ladder / fallback priority (ordered rungs) GKE ComputeClasspriorities (compose from the primitives below) (compose from the primitives below)
Promotion (climb back) GKE ComputeClass—active migration (application-driven re-request) (application-driven re-request)
Preflight / reserved capacity (higher-assurance rungs) DWS flex-start · Compute reservations EC2 Capacity Blocks for ML · On-Demand Capacity Reservations Azure Capacity Reservations
Cheaper / interruptible rungs (lower on the ladder) GKE Spot VMs EC2 Spot Instances Azure Spot Virtual Machines

Lineage

This pattern extends known work, and the post traces the ancestry in What This Stands On: opportunistic computing (Condor, ICDCS 1988), backfill scheduling (Mu'alem & Feitelson, IEEE TPDS 2001), and brownout / graceful degradation (Klein et al., ICSE 2014), with the Circuit Breaker (Nygard, Release It!, 2007) as its sibling—the breaker sheds demand where the ladder re-sources supply.

Development

$ pip install -e '.[dev]'
$ pytest

The Kubernetes example additionally needs Docker, kind, and kubectl; see examples/kubernetes/README.md.

License

Apache-2.0. See LICENSE.

Read the original on github.com ↗