This site is driven by two keys, like the machine it came from. Reading: j and k scroll, d and u move half a page, gg goes to the top, G to the bottom, H and L switch windows, ? opens the help. Space is the Neovim leader and handles content: Space then h home, r research, p projects, g gear, a about, / tags, or Space then a digit to jump to that window. Home is the tmux prefix and handles windows: Home then c opens a terminal, Home then & closes a window, Home then space goes to the next one. While focus is on the keyboard, h j k l move between keys and Enter opens one.

ResearchR AboutA Gear — G for gear — keyboard, terminal, editor, homelab.G
    ProjectsP Tags — / as in vim: search./
    ×
    Menu

    v-pipeline: Renting a GPU per Job, Not per Hour

    A personal video generation pipeline — the app and its files live in the homelab, the GPU is rented on RunPod Serverless and dies the moment a job finishes. Notes on idempotency, dry runs, and why restarting a worker can make you pay twice.

    A personal project in a closed repository — written up here for the architecture decisions rather than the output.

    The idea: generate video where a photo supplies the identity and a reference video supplies the motion. The app, the database and all the files stay in the homelab; the only thing rented is the GPU, on RunPod Serverless, and that GPU shuts down as soon as the job finishes.

    Why serverless

    A GPU rented by the hour means paying for idle time, and my usage is bursty — a handful of jobs one afternoon, then nothing for a week. Serverless inverts the relationship: one job, one charge, and nothing running in between.

    The consequence is that the part left in the homelab has to tolerate waiting, tolerate a power cut, and tolerate being restarted mid-flight.

    A job created twice is a job paid for twice

    This is the constraint that shaped the code more than any other.

    If the worker dies mid-job — a deploy, an OOM, a power cut — and comes back, it has to reattach to the same job at the provider, not create a new one. If the provider_job_id changes across a restart, that means one generation already paid for and abandoned, plus another one just started.

    So the smoke test doesn’t only exercise the happy path. It kills the worker mid-job, brings it back, and asserts the provider_job_id is byte-identical. If it differs the test fails — even if the video came out fine.

    A dry run you can actually trust

    Developing against something that costs real money on every attempt is unpleasant. So there’s a mock provider, and the DRY/LIVE toggle sits in the interface header, flipping providers without restarting the container. The choice is stored as a per-browser cookie, so a phone can run live while a laptop stays in dry run.

    The important part: the provider flag is stamped when the job is created, not read when the job runs. A job born in a dry-run session stays mock even if the toggle is flipped a minute later. Without that, one click at the wrong moment would send a queue of test runs to a real GPU.

    Failure paths can be summoned on purpose too. In dry-run mode, writing a particular token into the notes field triggers a particular failure — ffmpeg normalisation failing before any cost is incurred, the model refusing on safety grounds (flagged non-retryable, so no retry button appears), the job exceeding its execution timeout, no GPU capacity available, or generation succeeding but the download failing. All five are covered by the smoke test.

    Three modes, and one thing the model can’t do

    ModeBackground comes fromPrompt usedOutput
    animatethe photonovideo
    replacethe reference videonovideo
    editthe promptyesphoto

    The animation model doesn’t read text at all. So if the background is to be described in words, the route is two steps — edit first to produce a photo, then animate — not one job with a prompt. That’s a limit far better understood up front than discovered after twenty attempts that ignored the prompt.

    Status

    Still in progress. What’s settled: the job engine, idempotency across restarts, and the whole dry-run layer. The application itself is Python with file storage in the homelab, and ffmpeg normalises inputs before anything reaches the GPU — because rejecting an unusable input is free locally and is not free on the other side.

    id en
    rss gh in