all_lessons/kubernetes_genai/14lesson 14 / 18

Part IV - Customization and training jobs

Fine-Tuning Jobs on Kubernetes

Lesson 13 helped you decide whether to customize and how — prompt engineering, RAG, full fine-tune, or a parameter-efficient adapter like LoRA. Once you have chosen to train, you have to run that training somewhere, and a GPU training run is a fundamentally different beast from the serving deployments the rest of this track has built. A server is always-on and stateless; a training job runs to completion, holds many expensive GPUs at once, must survive node death without losing days of compute, and reads terabytes of data at line rate. This lesson takes the customization decision and turns it into a reproducible batch workload with the right scheduling, storage, checkpoint, and security boundaries — so that the artifact you hand back to the registry is one you can trust and re-create.

Source coverage
PDF Chapter 6 and Chapter 7: running tuning jobs, Kubeflow Trainer and distributed training frameworks, storage and data access, security boundaries, and training observability.
Linear position
Prerequisite: Lesson 13 (Model Customization Choices) — you have picked a customization technique and know it produces either a full model or a small adapter. Lesson 04 gave you the reproducibility discipline (pin the image, the model digest, the config) that this lesson now applies to a training artifact.
New capability: Run that customization as a Kubernetes Job — a run-to-completion batch workload — with gang-scheduled distributed workers, resumable checkpoints on fast storage, per-tenant data credentials, and rank-aware observability.
The plan
Five moves. (1) Establish the core distinction — a training Job is run-to-completion, not always-on, and that single fact reshapes scheduling, storage, and recovery. (2) Introduce Kubeflow Trainer and why distributed training needs all workers placed together (gang scheduling, previewed here, full treatment in lesson 15). (3) Make the checkpoint tradeoff quantitative — how often to save state is a knob trading checkpoint-write overhead against expected GPU time lost to preemption, with a worked number and a widget. (4) Sketch a real Job YAML — GPU request, shared data volume, checkpoint path, restart policy. (5) Failure modes, a reproducibility-and-security checklist, and the hand-off into batch scheduling and GPU fairness.

1 · A training job is not a service

Everything earlier in this track ran as a Deployment: a controller that keeps N identical pods alive forever, replacing any that die, scaling on load. That is exactly right for an inference server — you want continuous availability and fast, transparent replacement. A training run is the opposite shape. It starts, consumes resources for hours or days, produces an artifact, and terminates successfully. The Kubernetes primitive for that is a Job: a controller that runs one or more pods to completion and then stops, tracking success/failure rather than uptime.

Deployment → always-on, N replicas, restart forever, scale on traffic. The serving model.
Job → run-to-completion, terminal success/failure state, restart only on failure. The training model.

This is not a cosmetic difference. Because a Job terminates, its success is an event you can gate a pipeline on. Because it holds GPUs for the whole run rather than serving short requests, idle time is pure waste — a GPU that sits unused while the job waits for something costs real money every minute (an H100 rents for roughly $2-$4/GPU-hour on cloud, so a single idle 8-GPU node burns on the order of $20-$30/hour doing nothing). And because the run is long, the probability that some piece of hardware fails or gets preempted — evicted by the scheduler to free its GPUs for a higher-priority job (the mechanics of which are lesson 15's subject) — before the run finishes is no longer negligible, which is why checkpoints (§3) exist.

The mental model
A training job needs five things a service does not: all its workers scheduled together (gang scheduling), fast shared data at the input, resumable checkpoints so a node loss costs minutes not days, rank-aware logs so one slow worker is visible among dozens, and a queueing policy so a big run waits its turn rather than thrashing the cluster. Build a training platform around those five, not around the Deployment habits you carry from serving.

2 · Distributed training needs gang scheduling — and an operator that knows it

A small LoRA fine-tune of a 7B model might fit on one GPU and run as a single-pod Job — nothing exotic. But a full fine-tune of a large model, or any data-parallel run that wants throughput, spreads across many GPUs on many nodes. Those workers are not independent: they perform a synchronized all-reduce of gradients every step (the collective-communication machinery from the system_ml track). If worker 3 of 8 is not running, the other seven cannot make progress — they block on the collective, holding their GPUs idle while they wait.

This is the defining hazard of distributed training on a shared cluster. The default Kubernetes scheduler places pods one at a time, greedily. Hand it an 8-worker job and it may schedule 6 workers, then run out of free GPUs because some other job grabbed the last two — leaving you with 6 GPUs held hostage, idle, waiting for two that will never come, while the other job is in the same deadlock. The fix is gang scheduling: place all workers of a job together or place none of them (all-or-nothing), so a job only starts when its full quota is available. We preview the term here because it motivates everything in lesson 15, which builds the queueing and fairness machinery (Kueue, scheduler plugins) properly.

Kubeflow Trainer is the Kubernetes-native operator that orchestrates distributed training jobs — the successor to the older Kubeflow Training Operator and its per-framework CRDs (PyTorchJob, TFJob), consolidating them behind one TrainJob API. You hand it a TrainJob describing the runtime (PyTorch, etc.), the number of nodes, and the per-node resources; it creates the worker pods, wires up the rendezvous endpoints so the workers find each other, injects the per-worker environment each one needs — its rank (the worker's integer index, 0…N−1, that identifies it in the collective) and the world size (the total worker count) — and tracks the run to completion. It is the training-specific layer that a plain Job lacks.

Kubeflow Trainer
Kubernetes-native operator purpose-built for distributed training (PyTorch and friends). Manages worker creation, rendezvous, rank injection, and completion. The default if you want repeatable, owned training infrastructure.
Ray
General distributed-Python framework with a training library on top. A good fit if your org already runs Ray for data and serving workflows and wants one programming model end to end.
Lightweight libraries
Tools like Unsloth lower the barrier for small single-node fine-tunes (often LoRA). Less infrastructure to own, but they do not solve multi-node gang scheduling for you.

The platform choice is about repeatability and ownership, not raw training speed. All three can train a model; what differs is who operates the rendezvous, how the job integrates with the cluster's queueing, and how reproducible the run is six months later.

Gang scheduling fixes the worst case — a job that never fully starts. The subtler, everyday hazard is the straggler: all workers did start, but one is slower than the rest (a degraded NIC, a thermally throttled GPU, a noisy node), so every synchronized all-reduce stalls at the speed of the slowest rank, and the whole job's throughput drops to that worker's pace while the other GPUs idle on the barrier. A straggler is invisible in aggregate metrics — average GPU utilization still looks healthy — which is why a training platform needs rank-aware observability: logs and metrics tagged with each worker's rank, so you can see that rank 5 is taking 1.4× as long per step as ranks 0–4 and 6–7. Without per-rank visibility, a distributed job that has quietly halved its throughput looks identical to a healthy one. (Lesson 15 returns to the straggler as a scheduling and topology problem; here it is the reason rank-aware logging is a first-class platform requirement, not an afterthought.)

3 · How often to checkpoint — the quantitative knob

A checkpoint is periodically saved training state — model weights, optimizer state, the step counter, the data-loader position — written to durable storage so that if a worker dies or gets preempted, the run resumes from the last save instead of from step zero. On a long run this is not optional: over hours or days, hardware will fail or the scheduler will preempt your job for a higher-priority one. Without a checkpoint, every such event throws away all the GPU time since the run began.

So you checkpoint often. But checkpoints are not free: writing the full optimizer + weight state for a large model can be tens to hundreds of GB, and that write blocks (or at least taxes) training while it happens. Checkpoint too often and the writes dominate wall time; checkpoint too rarely and a single preemption costs hours. There is a sweet spot, and it has a number.

Worked number — the checkpoint interval tradeoff
Suppose each checkpoint write costs W = 2 minutes of stalled GPU time, and node failure or preemption hits your job on average once every MTBF = 6 hours (= 360 min). You checkpoint every T minutes. Overhead from writes per hour = 60/T × W minutes wasted. Expected loss per failure is the work since the last checkpoint, on average half an interval: T/2 minutes, occurring 60/360 times per hour. Total waste per hour ≈ (60/T)×2 + (60/360)×(T/2). At T = 10 min: writes cost 12 min/hr, lost work costs ≈ 0.83 min/hr → ~12.8 min/hr wasted (over-checkpointing). At T = 60 min: writes cost 2 min/hr, lost work ≈ 5 min/hr → ~7 min/hr. At T = 30 min: 4 + 2.5 = ~6.5 min/hr — near the minimum. The optimal interval scales as T* ≈ √(2·W·MTBF) ≈ √(2·2·360) ≈ 38 min here. Drag the widget to feel it.

Checkpoint interval — write overhead vs. expected lost work
The green curve is GPU time lost to checkpoint writes (falls as you checkpoint less often). The red curve is expected GPU time lost to preemption — on average half an interval of recompute per failure (rises as you checkpoint less often). The purple curve is their sum; the dot marks the minimum. Drag the interval and the failure rate and watch the sweet spot move.
Write overhead (min/hr)
Expected lost work (min/hr)
Total wasted (min/hr)
Optimal interval T* (min)
Show the core JS
// per hour: write overhead + expected recompute on failure
write = (60 / T) * W;                 // T smaller -> more writes -> more overhead
loss  = (60 / MTBF) * (T / 2);        // T larger  -> more lost work per failure
total = write + loss;
Tstar = Math.sqrt(2 * W * MTBF);      // analytic minimum of write+loss

Two refinements production platforms add. First, asynchronous / sharded checkpointing (each rank writes its shard in parallel, overlapping with compute) drives W down so you can afford to checkpoint more often. Second, the math above assumes checkpoint storage is fast and reliable — if it is not, W balloons and the whole curve shifts up, which is the first failure mode below.

4 · A training Job, concretely

Here is the shape of a single-pod fine-tuning Job — enough to see the four things a serving Deployment does not have: a GPU request, a fast shared data volume, a durable checkpoint path, and a restart policy.

apiVersion: batch/v1
kind: Job
metadata:
  name: lora-tune-llama-7b
spec:
  backoffLimit: 3            # retry the failed pod up to 3x, then mark Job failed
  template:
    spec:
      restartPolicy: OnFailure       # Jobs cannot use "Always"; OnFailure or Never
      containers:
      - name: trainer
        image: registry.internal/trainer@sha256:...   # pinned digest (lesson 04)
        command: ["python", "tune.py", "--config", "/cfg/run.yaml"]
        resources:
          limits:
            nvidia.com/gpu: 1        # the GPU request
        env:
        - name: HF_TOKEN             # data/model credential, per-job
          valueFrom: { secretKeyRef: { name: tenant-a-data, key: hf-token } }
        volumeMounts:
        - { name: data, mountPath: /data, readOnly: true }   # fast shared input
        - { name: ckpt, mountPath: /ckpt }                   # durable checkpoints
      volumes:
      - name: data                   # high-throughput shared dataset
        persistentVolumeClaim: { claimName: training-data-fast }
      - name: ckpt                   # durable, resumable checkpoint store
        persistentVolumeClaim: { claimName: ckpt-tenant-a }

For a multi-node distributed run you would not hand-write the rank wiring; you would hand Kubeflow Trainer a TrainJob that declares numNodes and the per-node resources, and it generates the gang-scheduled worker pods, the rendezvous, and the rank/world-size env for you. The single-Job sketch above is the mental anchor — the distributed version is the same four concerns multiplied across workers, plus the all-or-nothing placement from §2.

Note the credential handling. The data token is a per-job secretKeyRef scoped to this tenant's dataset — not a broad service-account permission the container inherits, and never baked into the image. That separation is what keeps one tenant's training run from reading another's data, and it is the third checklist item below.

5 · Where training jobs go wrong

Failure modes

  • Partial placement deadlock. One worker of a distributed job cannot schedule (no free GPU), so the others sit blocked on the gradient all-reduce, holding GPUs idle. The signal: GPUs allocated but near-zero utilization, and a job stuck in a partially-running state. This is exactly what gang scheduling prevents (lesson 15).
  • Slow checkpoint storage dominates wall time. Checkpoints written to a slow or unreliable store push W high — the write stalls eclipse training, and a flaky store can corrupt the one checkpoint you needed to resume from. The signal: long periodic GPU-idle stalls aligned with the checkpoint interval, or resume failures.
  • Secret leakage. Training credentials baked into images, printed into logs, or left in a shared notebook leak data-access tokens to anyone who can pull the image or read the logs. The signal: tokens appearing in kubectl logs or in image layers on scan.
  • Expanded trust boundary. Frameworks expose dashboards, job-submission endpoints, and worker-to-worker comms (a Ray dashboard or PyTorch rendezvous left unauthenticated). Treat a training job as untrusted code holding expensive devices and sensitive data until the platform proves otherwise — scope network access and namespaces.
  • Irreproducible artifact. The run finished and the model is better, but nobody recorded the data version or seed — so you cannot tell if it was better data, different hyperparameters, or environment drift, and you cannot reproduce it.

Implementation checklist

  • Do you record code revision, image digest, base-model digest, adapter config, seed, hyperparameters, and hardware shape as one run artifact? (Reproducibility, ties to lesson 04.)
  • Do you checkpoint often enough to survive preemption/node-loss without losing hours of GPU time — at an interval near √(2·W·MTBF), not by guess?
  • Is checkpoint storage fast and durable enough that W stays small and resumes actually work?
  • Are data-access credentials scoped per job and per tenant via Secrets — not baked into images, not broad service-account grants?
  • Do you collect rank-aware logs and metrics so one slow/straggler worker is visible among many?
  • Is the job gang-schedulable (all-or-nothing placement) so it never holds GPUs idle waiting for a missing rank?
  • Are framework dashboards and rendezvous endpoints authenticated and network-scoped?

Checkpoint exercise

Try it
You are running a 5-day full fine-tune on 16 GPUs. Checkpoint writes take 90 seconds each, and your cluster preempts training jobs about once every 4 hours. Compute the rough optimal checkpoint interval with T* ≈ √(2·W·MTBF), then estimate how many GPU-hours per day you waste at that interval versus checkpointing every 5 minutes. Finally, name the one storage property that, if it degraded, would invalidate your whole calculation.

Where this points next

This lesson treated a single job in isolation — its workers, its storage, its checkpoints. But a real cluster runs many training jobs from many teams against a finite pool of GPUs, and the partial-placement deadlock in §5 showed that the default scheduler cannot handle that safely. The next lesson, Batch Scheduling and GPU Fairness, builds the machinery that was previewed here as "gang scheduling": all-or-nothing placement, job queues, quotas, and fair-share policies (Kueue and scheduler plugins) that decide which job runs when, so big runs wait their turn instead of thrashing the cluster and stranding GPUs.

Takeaway
A fine-tuning run is a run-to-completion Job, not an always-on Deployment, and that single fact reshapes everything: it needs all its distributed workers placed together (gang scheduling, or it deadlocks holding GPUs idle), fast shared input data, and resumable checkpoints so a node loss costs minutes not days. How often to checkpoint is a real knob — write overhead falls and expected lost work rises with the interval, with an optimum near √(2·W·MTBF) — and it collapses if checkpoint storage is slow or flaky. Kubeflow Trainer (or Ray, or a lightweight library) gives the training-specific orchestration a plain Job lacks. And because a training job is untrusted code holding expensive GPUs and sensitive data, scope its credentials per job and tenant, authenticate its dashboards, and capture code/data/seed/config as one artifact so the result is reproducible.

Interview prompts