Repository navigation
feat: GPU profiling kit and capacity note template - #4
matthewberry wants to merge 3 commits into
Conversation
profiling/gpu_profile.py runs a series of input sizes in one Job on the mmli2 GPU node, mirroring how the backend launches a GPU tool, and records wall time, peak VRAM over the card's baseline and peak RSS per size, then fits fixed vs per-item cost. It refuses to launch while any pod holds or waits for the GPU, or a Job uses the nvidia runtime without requesting it. CAPACITY_NOTE.md and 'gpu_profile.py capacity' project GPU queue wait at 1x/5x/10x traffic (M/G/1); the README derives maxItems and the job deadline from the profile. Part of #2
matthewberry
left a comment
There was a problem hiding this comment.
Review of the GPU profiling kit. Nothing was run. Each finding comes from reading the diff, plus a comparison with mmli-backend origin/main (job.tpl.yaml, chart/values*.yaml). Treat them as hypotheses.
The guard's "GPU busy" rule holds for what it checks. It skips pods that are neither Pending nor Running. It counts any pod that requests nvidia.com/gpu, and any Job-owned pod with runtimeClassName: nvidia, whether Pending or Running, as a blocker. Its gaps are about when it checks (finding 3) and what it can't see (finding 4).
profiling/gpu_profile.py:142: one ConfigMap, applied client-side. Every size'sjob.jsongoes into one ConfigMap, applied withkubectl apply. That copies the whole object into the last-applied annotation, which is capped at 262144 bytes. Realistic inputs, such as sizes 1–500 of ~500-residue proteins, go past that, and the apply fails withmetadata.annotations: Too long. So the profile can't reach the sizes it exists to find. Usekubectl create, server-side apply, or the shared volume.profiling/gpu_profile.py:145: a deadline hit loses every record silently. WhenactiveDeadlineSecondskills the Job, the pod is gone.kubectl logsthen returns nothing, and nobody checks its exit code.parse_logreturns[], and the report says fewer than two sizes succeeded, even though the small sizes finished. There's also no per-size timeout.profiling/gpu_profile.py:136: the guard runs once, before the apply. A runtime-class tool can start mid-run: cleandb-mepesm, aceretro, novostoic-enzrank, oed-*, reactionminer. It then adds VRAM and compute contention to later sizes, and the fit absorbs that as per-item cost. Re-runninggpu_blockerswhile waiting or after the run, and marking the run tainted, would catch it.profiling/gpu_profile.py:58: resident services aren't constant. chemscraper-services' GPU use depends on chemscraper jobs that run on CPU worker nodes, which the guard never sees. A spike inside one size's window counts as that tool's VRAM and time, because the driver uses max(during) minus min(baseline).profiling/CAPACITY_NOTE.md:45:service_sis too short. It is defined as the tool process's run time. But a GPU-requesting pod holds the GPU for its whole life: image pull (imagePullPolicy: Always), the init download, and post-job. Underestimating rho makes the Pollaczek–Khinchine wait look bounded at 5× and 10× when it may not be. Use the pod's scheduled-to-terminated time instead.profiling/driver.py:90:ru_maxrssis the peak of one process, not the sum. It also leaves out/dev/shm, which counts against the cgroup limit. A tool with 4×3 GiB DataLoader workers plus a 6 GiB main process reports ~6 GiB while the cgroup peaks above 18 GiB, and a memory limit derived from that number gets OOMKilled. Read the cgroup'smemory.peakinstead.profiling/job.yaml:84:/dev/shmis always 8Gi. The backend only mounts dshm when values setshmSizeLimit(today only ezspec-inference, at 4Gi). Every other tool gets 64Mi. So the profile can pass a PyTorch tool that crashes in prod with "insufficient shared memory". Default to none, or mirror the tool's values.profiling/job.yaml:70: the cache mount doesn't match the existing tools. ml-simplefold mounts.cacheat/root/.cache/torch/hub, and cleandb-mepesm and ezspec-inference mount.hubundertorch/hub. Mounting at/root/.cachenests downloads inside prod's hub dir, and it masks weights baked into the image.profiling/job.yaml:49: it doesn't "mirror" the backend's resources or entrypoint. It defaults to requests==limits of 4 CPU / 16Gi. The backend gives ml-simplefold, for example, 4/24Gi requests and 16/32Gi limits, so the profile OOMKills sizes that run in prod.command: python /profile/driver.pyalso bypasses an image ENTRYPOINT that may activate a conda env.profiling/gpu_profile.py:54: two doc errors. "cleandb-mepesm and seven other tools" is wrong:values.mmli2.prod.yamlhas six others, and the PR body says both six and eight. Also, CAPACITY_NOTE.md:29 says these tools' memory "goes in the budget above", but no budget appears above it.
- create, not apply: client-side apply copies the ConfigMap into a 256 KiB annotation (469 KB for sizes up to 500 of 500-residue inputs). Inputs over a ConfigMap's ~1 MiB are refused before anything is created. - Follow the pod log from the start, so records survive a deadline kill; profile.md lists sizes that didn't finish. - Re-check for GPU jobs every 10 s while running; profile.json records them under contended_by and profile.md marks the profile contended. - Memory peak from the cgroup (mem_peak_mib), which counts workers and /dev/shm; ru_maxrss reported 213 MiB for three concurrent 200 MiB workers. - /dev/shm only with --shm, as the backend mounts it only for shmSizeLimit. - Separate requests and limits, defaulting to ml-simplefold's. - Docs: service time is the whole pod's GPU hold; VRAM baseline caveat; six other runtime-only tools, not seven; the ENTRYPOINT assumption.
|
Replies to the review. The fixes are all in bac0ba9. Tests: 23 → 32, all passing (
|
matthewberry
left a comment
There was a problem hiding this comment.
Re-review of bac0ba9. I read the diff and the dispositions comment. I did not run the kit's tests in this pass.
The fixes for 1, 3, 5, 6, 7, 9 and 10 look right:
kubectl createdoesn't write the last-applied annotation.- The size cap runs before anything is created.
- The pod template carries
app: gpu-profile, so_other_podsexcludes the run's own pod correctly. - The cgroup sampler counts workers and
/dev/shm. /dev/shmis mounted only with--shm.- Requests and limits are now set separately.
I accept the declines: 4 is documented, and 8 follows the template's own convention.
New points:
profiling/__pycache__/*.cpython-315.pyc: bytecode is committed. bac0ba9 adds four of these files (driver,gpu_profile,inputs,test_gpu_profile), and the repo has no.gitignoreentry for__pycache__. Remove them and add the ignore.profiling/gpu_profile.pyrun(): the log follower can still lose records silently. Fix 2 was verified only by reading the code, and I've only read it too:- stderr goes to
DEVNULL. Ifkubectl logs -f job/<name>fails to attach (e.g. it gives up before the pod exists, or the pod is never Running), nothing reports it.logcomes back empty, and the report says no sizes finished, which is finding 2's symptom again. Capturing stderr and printing it when no records come back would make that visible. logs.communicate(timeout=120)raisesTimeoutExpiredif the stream doesn't close within 120 s of the Job ending. Thefinallydeletes the Job, butprofile.json/profile.mdare never written, and thekubectlprocess is never killed. Catching the timeout, killing the process and parsing what was read would keep the records.- Please confirm on staging that
kubectl logs -f job/X --pod-running-timeout=…started while the pod is still Pending waits for it, with your kubectl version.
- stderr goes to
- Minor,
_other_pods: it excludes everyapp=gpu-profilepod in every namespace. A second person's profiling run that starts mid-run isn't recorded as contention. Filtering on this run's own Job name (thejob-namelabel) instead would keep it.
…e own-pod exclusion - kubectl logs stderr is captured: a failed attach is reported as log_error in profile.json and profile.md instead of looking like a run with no records. - If the stream outlives the Job by LOG_DRAIN_TIMEOUT_S, kill it and keep what arrived, rather than raising before the profile is written. - Contention polling skips only this run's pods (job-name label), so another profiling run counts. - Untrack __pycache__ committed in bac0ba9; add .gitignore.
|
Replies to the second pass. All four are fixed in e7600c0. Tests: 32 → 35, all passing. I wrote the three new
|
matthewberry
left a comment
There was a problem hiding this comment.
Round 3 (e7600c0): all four round-2 findings are fixed. I read the diff only; I didn't run the tests.
- The four
.pycfiles are untracked, and.gitignorenow covers__pycache__/. - kubectl's stderr is now captured. A non-zero exit is recorded as
log_errorinprofile.jsonand shown inprofile.md. - If the log stream hasn't closed
LOG_DRAIN_TIMEOUT_Safter the Job ends, it is killed, the records already read are kept, and the profile is still written. Skipping the returncode check whenlog_erroris already set correctly keeps the kill's negative returncode from being reported twice. _other_pods(name)now excludes only pods labeledjob-name=<name>, which the Job controller sets, so another profiling run counts as contention.
No new findings. One note: when a deadline kill deletes the pod, kubectl logs -f may exit non-zero, and that run will now show "Log incomplete" next to its "Did not finish" sizes. That's accurate, so no change is needed.
Part of #2. Everything except the last criterion, the run on the mmli2 GPU node, which waits for a go-ahead (commands below).
What
profiling/, a kit for the profile that ends every tool's compute phase:gpu_profile.py run: runs a series of input sizes in one Job instaging(ConfigMap plus Job fromjob.yaml, created rather than applied), follows the pod log so records survive a deadline kill, re-checks for GPU contention every 10 s, cleans up, and writesprofile.jsonandprofile.md. Per size it records wall time, card VRAM baseline and peak (nvidia-smi every 200 ms), peak memory from the cgroup (counting workers and/dev/shm), and the single largest process's peak RSS. It fitsfixed + per_item × sizefor time and for VRAM over the baseline.job.yaml: mirrors how the backend launches a GPU tool: thenvidia.com/gpu.presentselector, thenvidia.com/gputoleration,runtimeClassName: nvidia, a 1-GPU request, root, the/root/.cachemount onmmli-shared-job-data, and/dev/shmonly when--shmis given (as the backend does forshmSizeLimit). Requests and limits are set separately, defaulting to ml-simplefold's. It runs the samepython -m wrapper.entrypointon ajob.json. Each size is a fresh process, so model load counts as fixed cost every time. One unmeasured warm-up run comes first, so a cold weight cache doesn't skew the first point.gpu_profile.py check, run before everyrun): it blocks on any pod that holds or waits fornvidia.com/gpu, and on any Job pod under the nvidia runtime without a GPU request (cleandb-mepesm and six others, which share the card unqueued). Resident non-Job pods on the nvidia runtime (the gpu-operator DaemonSets and chemscraper-services) don't block; they're part of the baseline.inputs.py:make_job(size), the per-tool hook. Here it's implemented for the sample tool.CAPACITY_NOTE.mdplusgpu_profile.py capacity: mean GPU queue wait and utilization at 1×, 5× and 10× traffic, treating the GPU as one FIFO M/G/1 queue (Pollaczek–Khinchine) over the GPU-requesting tools.profiling/README.md: how to run it, and how to derivemaxItems(the smaller of the time limit, the VRAM limit, and the largest size that succeeded) and the job deadline from the profile.python -m unittest discover -s profiling; standard library only).Found along the way (not changed here)
create_jobreadsactiveDeadlineSecondsonly fromkubernetes_jobs.defaults(259200 s, 3 days). A hung GPU job can hold the only GPU for 3 days. The README says so, and the capacity note records the derived value until the backend supports one.chart/values.mmli2.*.yaml, only ml-simplefold (through the base values), ezspec-unidock and ezspec-inference requestnvidia.com/gpu. Seven more (cleandb-mepesm and six others) run under the nvidia runtime without a request.How it was checked
python:3.12-slim) on sizes 1, 10 and 100.gpu_profile.py render … | kubectl apply --dry-run=client --validate=strict -f -passes.gpu_profile.py checkagainst mmli2 (read-only): no blockers, with the six resident pods listed.The GPU run (criterion 5), pending a go-ahead
cd profiling python3 gpu_profile.py check python3 gpu_profile.py run --image ghcr.io/moleculemaker/job-template:sha-3a5c0da --tool job-template --sizes 1,10,100 --out out-job-templateIt creates and then deletes
gpu-profile-job-template-<ts>(a ConfigMap and a Job) instaging, holds the GPU for about a minute, and createsuws/jobs/job-template/.cacheon the shared volume. The sample tool is CPU-only, so this checks the mechanics (scheduling, the nvidia-smi sampling, logs, the report), not real GPU numbers. VRAM over the baseline should come out near 0.