gpushift 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. gpushift-0.1.0/.gitignore +7 -0
  2. gpushift-0.1.0/CHANGELOG.md +38 -0
  3. gpushift-0.1.0/LICENSE +21 -0
  4. gpushift-0.1.0/PKG-INFO +354 -0
  5. gpushift-0.1.0/README.md +323 -0
  6. gpushift-0.1.0/examples/hf_trainer_resumable.py +80 -0
  7. gpushift-0.1.0/examples/lightning_resumable.py +51 -0
  8. gpushift-0.1.0/examples/pytorch_resumable.py +67 -0
  9. gpushift-0.1.0/pyproject.toml +62 -0
  10. gpushift-0.1.0/src/gpushift/__init__.py +7 -0
  11. gpushift-0.1.0/src/gpushift/__main__.py +4 -0
  12. gpushift-0.1.0/src/gpushift/_entry.py +14 -0
  13. gpushift-0.1.0/src/gpushift/_proc.py +152 -0
  14. gpushift-0.1.0/src/gpushift/autostart.py +97 -0
  15. gpushift-0.1.0/src/gpushift/checkpoint.py +140 -0
  16. gpushift-0.1.0/src/gpushift/cli.py +1100 -0
  17. gpushift-0.1.0/src/gpushift/config.py +269 -0
  18. gpushift-0.1.0/src/gpushift/control.py +658 -0
  19. gpushift-0.1.0/src/gpushift/cpustat.py +171 -0
  20. gpushift-0.1.0/src/gpushift/daemon.py +415 -0
  21. gpushift-0.1.0/src/gpushift/db.py +214 -0
  22. gpushift-0.1.0/src/gpushift/gpumem.py +103 -0
  23. gpushift-0.1.0/src/gpushift/gpus.py +123 -0
  24. gpushift-0.1.0/src/gpushift/hook.py +317 -0
  25. gpushift-0.1.0/src/gpushift/notify.py +70 -0
  26. gpushift-0.1.0/src/gpushift/paths.py +76 -0
  27. gpushift-0.1.0/src/gpushift/runner.py +178 -0
  28. gpushift-0.1.0/src/gpushift/scheduler.py +434 -0
  29. gpushift-0.1.0/src/gpushift/ui.html +574 -0
  30. gpushift-0.1.0/src/gpushift/ui.py +170 -0
  31. gpushift-0.1.0/tests/conftest.py +162 -0
  32. gpushift-0.1.0/tests/jobs/counter.py +31 -0
  33. gpushift-0.1.0/tests/test_hook.py +159 -0
  34. gpushift-0.1.0/tests/test_integration.py +552 -0
  35. gpushift-0.1.0/tests/test_scheduler.py +338 -0
  36. gpushift-0.1.0/tests/test_units.py +264 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ checkpoints/
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-12)
4
+
5
+ First release.
6
+
7
+ - `gpushift` CLI and scheduler daemon: presets (`--light`, `--one`, `--two`,
8
+ `--exclusive`, `--cpu`), slots, start-time estimates learned from earlier runs,
9
+ and `status --json` with a stable schema.
10
+ - Scheduling: first come, first served (or sessions take turns, `fair_share = true`); a waiting
11
+ multi-GPU job can't starve, while jobs that finish in time may use idle GPUs;
12
+ jobs with a known memory need (`--vram`, or learned from earlier runs) share GPUs;
13
+ short jobs may run before long ones when that cuts the total waiting, without
14
+ pushing any job back more than an hour or half its run time.
15
+ - Free mode: `gpushift free` checkpoints resumable jobs, restarts `--restartable`
16
+ ones and freezes the rest, and says which GPU frozen jobs still hold;
17
+ `gpushift resume` continues everything. Survives reboots and daemon restarts.
18
+ - Automatic free mode while the user runs heavy GPU programs (games, Blender, ...),
19
+ detected from per-process GPU memory, with automatic resume afterwards.
20
+ - Notifications: the submitting Claude session is told when its jobs end, and
21
+ desktop notifications report failures.
22
+ - `--after <job>`: a job starts only once the jobs it depends on finished
23
+ successfully (train, then eval); it is cancelled if one of them fails.
24
+ - `gpushift ui`: a local web dashboard with the GPUs, CPU and drives, a timeline
25
+ of what runs where and when, running jobs with their logs, the queue (drag to
26
+ reorder) and controls. Time a job spends frozen doesn't count as run time.
27
+ - Jobs run with the submitter's cwd and environment. A command given as one
28
+ string runs in a shell: on Windows, the Git Bash it was submitted from (what
29
+ Claude Code's Bash tool uses) unless it uses backslash paths, otherwise
30
+ cmd.exe; `/bin/sh` elsewhere. Missing programs or scripts and half-quoted
31
+ commands are rejected at submit; a failed job's note includes its last output line.
32
+ - `gpushift.checkpoint`: a dependency-free helper that makes training scripts
33
+ resumable, with examples for plain PyTorch, HF Trainer and Lightning.
34
+ - Autostart at login on Windows (HKCU Run key), Linux (systemd user service)
35
+ and macOS (LaunchAgent).
36
+ - Claude Code plugin: the `gpu-queue` skill; `/gpushift:queue`, `/gpushift:priority`,
37
+ `/gpushift:cancel`, `/gpushift:free` and `/gpushift:resume`; a hook that redirects
38
+ direct GPU launches to the queue, and one that reports ended jobs.
gpushift-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edward Glockner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,354 @@
1
+ Metadata-Version: 2.5
2
+ Name: gpushift
3
+ Version: 0.1.0
4
+ Summary: Share one GPU machine between AI coding agents and yourself: a fair GPU job queue that steps aside when you need the GPUs.
5
+ Project-URL: Homepage, https://github.com/EdwardGlockner/gpushift
6
+ Project-URL: Issues, https://github.com/EdwardGlockner/gpushift/issues
7
+ Project-URL: Changelog, https://github.com/EdwardGlockner/gpushift/blob/main/CHANGELOG.md
8
+ Author: Edward Glockner
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,claude-code,cuda,gpu,machine-learning,queue,scheduler
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: psutil>=5.9
24
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
25
+ Provides-Extra: demo
26
+ Requires-Dist: pillow>=10; extra == 'demo'
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == 'dev'
29
+ Requires-Dist: ruff>=0.6; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # gpushift
33
+
34
+ [![ci](https://github.com/EdwardGlockner/gpushift/actions/workflows/ci.yml/badge.svg)](https://github.com/EdwardGlockner/gpushift/actions/workflows/ci.yml)
35
+ ![python](https://img.shields.io/badge/python-3.10%2B-blue)
36
+ ![platforms](https://img.shields.io/badge/platforms-linux%20%7C%20windows%20%7C%20macos-lightgrey)
37
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
38
+
39
+ Share one GPU machine between your Claude Code agents and yourself. Agents queue
40
+ their GPU work instead of launching it, so they never crash each other's runs;
41
+ jobs run in the order they were queued, share a GPU when they fit, and step aside when you start a game
42
+ or Blender, then carry on when you're done.
43
+
44
+ - **One queue for every session.** A Claude Code hook stops agents that launch
45
+ training directly and tells them how to queue it.
46
+ - **The machine is still yours.** `/gpushift:free` (or just starting a heavy GPU
47
+ program) makes running jobs save a checkpoint and step aside; they resume later.
48
+ - **Efficient.** First come, first served, but short jobs fill idle GPUs or go
49
+ ahead of long ones when that cuts everyone's waiting, and jobs whose memory use
50
+ is known share a GPU.
51
+ - **You stay informed.** Agents are told when their jobs end, you get desktop
52
+ notifications for failures, and a web dashboard shows everything.
53
+ - Windows, Linux and macOS, any number of NVIDIA GPUs (or none). No configuration.
54
+
55
+ <picture>
56
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/EdwardGlockner/gpushift/main/docs/img/gpushift-dark.svg">
57
+ <img alt="Claude sessions submit GPU jobs to one shared queue, which runs one job per GPU. A hook blocks direct launches. /gpushift:free makes jobs checkpoint and step aside, /gpushift:resume continues them, and starting a game or Blender does the same automatically." src="https://raw.githubusercontent.com/EdwardGlockner/gpushift/main/docs/img/gpushift-light.svg">
58
+ </picture>
59
+
60
+ ## Quickstart
61
+
62
+ ```bash
63
+ uv tool install gpushift # or: pipx install gpushift
64
+ gpushift init # detect GPUs, write the config, start the daemon (and at login)
65
+ ```
66
+
67
+ Then, in Claude Code:
68
+
69
+ ```text
70
+ /plugin marketplace add EdwardGlockner/gpushift
71
+ /plugin install gpushift@gpushift
72
+ ```
73
+
74
+ Agents now queue their GPU work. You can too:
75
+
76
+ ```text
77
+ $ gpushift submit --one --eta 40m -- python train.py --epochs 90
78
+ job 14 queued (one: 1 GPU, 2 slots) tag=resnet
79
+ position 2 of 3, estimated start: ~40m (14:52)
80
+ logs: gpushift logs 14 -f
81
+ ```
82
+
83
+ `gpushift status` (or `gpushift ui` for the dashboard) shows what runs where,
84
+ what waits, and when it should start:
85
+
86
+ ```text
87
+ gpushift mode: normal | daemon: running | slots: 4/4 used
88
+
89
+ GPU 0 GTX 1070 6.1/8.0 GB job 12 one resnet 1h02m python train.py --cfg a.yaml
90
+ GPU 1 GTX 1060 6GB 2.3/6.0 GB (shared) job 15 light eval 3m python eval.py --ckpt last.pt
91
+ job 16 light eval 1m python eval.py --ckpt best.pt
92
+
93
+ Queue (2 waiting)
94
+ pos id preset tag waiting est. start command
95
+ 1 17 two llama-sft 12m ~40m (14:52) torchrun --nproc_per_node 2 sft.py
96
+ 2 18 cpu data 3m ~41m (14:53) python tokenize.py
97
+ ```
98
+
99
+ ## Submitting jobs
100
+
101
+ | Preset | Use for | Gets |
102
+ |---|---|---|
103
+ | `--light` | Small evals/inference that fit in a fraction of VRAM | Shares the *shared GPU* with other light jobs, never with a training run |
104
+ | `--one` (default) | A normal single-GPU job | 1 GPU (a whole one, unless its memory need is known; see below) |
105
+ | `--two` | Multi-GPU (DDP, torchrun) | All GPUs gpushift manages |
106
+ | `--exclusive` | Benchmarks, jobs that need the whole box | Everything; nothing else runs alongside |
107
+ | `--cpu` | Preprocessing, CPU-only work | No GPU (`CUDA_VISIBLE_DEVICES=""`) |
108
+
109
+ Options:
110
+
111
+ - `--eta 40m`: how long it takes. Gives better start estimates and lets the job use
112
+ idle GPUs (below). Without it, gpushift learns from earlier runs of the same command.
113
+ - `--vram 6G`: GPU memory it needs, so it can share a GPU. Also learned from earlier runs.
114
+ - `--restartable`: safe to stop and run again (it skips finished work); see free mode.
115
+ - `--after 12` (or `--after 12,13`): start only once those jobs finished successfully,
116
+ e.g. an eval after its training run. Until then it doesn't hold anything else up; if
117
+ one of them fails or is cancelled, this job is cancelled too. Use it whenever order
118
+ matters: queue order alone doesn't make jobs run one after another (two jobs can run
119
+ side by side on two GPUs). A job can't be moved ahead of a job it waits for.
120
+ - `--priority` (front of the queue), `--tag`, `--gpus N` / `--slots N` (override the preset).
121
+
122
+ The job runs with the **cwd and environment of the submit call** (your venv/conda
123
+ env, env vars), with `CUDA_VISIBLE_DEVICES` set to its GPUs and
124
+ `CUDA_DEVICE_ORDER=PCI_BUS_ID`, so GPU numbers match `nvidia-smi`. Give the command
125
+ as separate words, or as one quoted string to run it in a shell: on Windows, the Git
126
+ Bash it was submitted from (what Claude Code's Bash tool uses), or cmd.exe if it uses
127
+ backslash paths or comes from cmd/PowerShell; `/bin/sh` on Linux and macOS. A
128
+ missing program or script is rejected right away, not when the job starts.
129
+
130
+ ## How jobs are scheduled
131
+
132
+ **First come, first served.** Jobs start in the order they were queued. Jobs moved
133
+ up with `--priority` / `gpushift priority`, and evicted jobs waiting to resume, go
134
+ first. With `fair_share = true`, Claude sessions (and you, from a terminal) take
135
+ turns instead: the next job comes from whoever has used the least GPU time in the
136
+ last 24 hours, so one session's list of thirteen jobs doesn't hold up everyone else.
137
+
138
+ **Sharing GPUs.** A job whose memory need is known (`--vram`, or the peak of earlier
139
+ successful runs of the same command plus 15%) can share a GPU with other such jobs,
140
+ as long as they fit in what's left of its memory. Shared GPUs fill up first, so
141
+ whole GPUs stay free for jobs that need one. Jobs with an unknown need always get a
142
+ whole GPU. `packing = false` turns this off.
143
+
144
+ **No starvation, no idle GPUs.** A job waiting for several GPUs never starves behind
145
+ a stream of small ones: it holds back the jobs behind it. They may still use a GPU
146
+ that would otherwise sit idle, if their estimate says they finish before the
147
+ waiting job could start anyway. When a running job is past its estimate, nobody
148
+ knows when it ends, so then nothing jumps ahead.
149
+
150
+ **Short jobs before long ones, within limits.** Strict order can leave a GPU idle
151
+ for hours: a 4-hour one-GPU job starts, and a batch of 15-minute whole-machine jobs
152
+ queued after it waits for it. Before starting anything, gpushift
153
+ forecasts the queue and checks whether running some jobs ahead of their turn cuts
154
+ the total waiting. It does that only if no job starts more than an hour (or half its
155
+ own run time, if that is longer) later than it would in queue order, counted from
156
+ the first time it was passed, and never for jobs moved up with `--priority`. Equal
157
+ jobs keep their order. `reorder_slack` sets the hour; 0 turns this off.
158
+
159
+ **Slots** are the machine's concurrency budget (default: 2 per GPU). `--light`,
160
+ `--cpu` and GPU-sharing jobs use 1, `--one` and `--two` use 2, `--exclusive` all.
161
+
162
+ **GPUs used outside gpushift**: an unclaimed GPU only counts as free if at least
163
+ `gpu_free_perc` (default 75%) of its memory is free.
164
+
165
+ ## When you need the GPUs
166
+
167
+ ```bash
168
+ gpushift free # pause the queue and clear the GPUs (/gpushift:free in Claude Code)
169
+ gpushift resume # carry on (/gpushift:resume)
170
+ ```
171
+
172
+ `free` pauses the queue, then:
173
+
174
+ - **Resumable jobs** (they use `gpushift.checkpoint`, below) save a checkpoint, exit and
175
+ go back to the front of the queue. A job that doesn't exit within `evict_timeout`
176
+ (300 s) is frozen instead.
177
+ - **Restartable jobs** (`--restartable`) are stopped and go back to the front, to run
178
+ again. Use it for jobs that skip work they already finished, like most eval scripts.
179
+ - **Other jobs are frozen.** They stop computing but keep their GPU memory, and `free`
180
+ says which GPU they hold and how to release it.
181
+
182
+ `free --soft` freezes everything instead (instant, but all GPU memory stays in use).
183
+ Free mode survives reboots. Agents can't run `free` or `resume`; only you can.
184
+
185
+ **Automatically.** When a program you started yourself uses the GPU heavily (a game,
186
+ Blender, your own training script), gpushift frees the GPUs after 10 seconds and
187
+ resumes two minutes after the last such program closes. "Heavily" means over 1 GB of
188
+ GPU memory, or any use by a program you list in `[auto_free] apps`; the desktop,
189
+ browsers and chat apps are ignored. A free you start by hand is never resumed for you,
190
+ and if you resume by hand, gpushift waits for the program to close before freeing again.
191
+
192
+ ## Notifications and the dashboard
193
+
194
+ - **Agents are told when their jobs end.** The plugin adds a short note to the session
195
+ that submitted the job ("job 34 (r2-trial) failed: exit code 2 after 0s: can't open
196
+ file ...") on its next step, so agents don't need to poll.
197
+ - **Desktop notifications** for failed jobs and automatic free/resume
198
+ (`[notify] desktop = "all"` for every job, `"off"` for none).
199
+ - **`gpushift ui`** opens a live dashboard at `http://127.0.0.1:8237`: GPUs and their
200
+ memory, load, temperature and power, the CPU, free space on each drive, a timeline
201
+ of what runs on which GPU and when, running jobs and their logs, the queue (drag a
202
+ job by its handle to move it), recent results with failure reasons, and buttons to
203
+ free/resume or cancel. It only listens on your own machine.
204
+
205
+ ## Commands
206
+
207
+ | Command | |
208
+ |---|---|
209
+ | `gpushift submit [preset] [options] -- <cmd>` | Queue a job (`--json` for machine output) |
210
+ | `gpushift status [--json]` | GPUs, running jobs, queue, estimated starts, mode |
211
+ | `gpushift ui` | Open the web dashboard |
212
+ | `gpushift logs <id> [-f] [-n N]` | Job output |
213
+ | `gpushift wait <id> [--timeout 30m]` | Block until the job ends; exit code mirrors the job |
214
+ | `gpushift show <id>` | One job's details |
215
+ | `gpushift priority <id>` / `gpushift swap <a> <b>` | Reorder the queue |
216
+ | `gpushift cancel <id>...` / `gpushift retry <id> [--priority]` | Cancel / queue a copy |
217
+ | `gpushift free [--soft]` / `gpushift resume` | Free the machine / continue |
218
+ | `gpushift init` / `gpushift doctor` / `gpushift config [--edit]` | Setup, health check, config |
219
+ | `gpushift start` / `gpushift stop` / `gpushift autostart on\|off` | Daemon control |
220
+
221
+ `gpushift status --json` has a stable schema (`"schema": 1`) for scripts and agents.
222
+
223
+ ## Claude Code plugin
224
+
225
+ ![An agent runs python train_lora.py; the gpushift hook blocks it and the agent submits it to the queue instead](https://raw.githubusercontent.com/EdwardGlockner/gpushift/main/docs/img/gpushift-hook.gif)
226
+
227
+ - **Skill `gpu-queue`**: never launch GPU work directly; how to pick a preset; check
228
+ `gpushift status` first; give an ETA and memory need; `--restartable` for jobs that
229
+ can rerun; `--priority` only when the user asks; never free/resume; make new
230
+ training scripts resumable; keep working while jobs run.
231
+ - **Slash commands**: `/gpushift:queue`, `/gpushift:priority <id>`, `/gpushift:cancel <id>`,
232
+ `/gpushift:free [--soft]`, `/gpushift:resume`.
233
+ - **Hooks**: before a Bash/PowerShell command, block direct GPU launches and show the
234
+ `gpushift submit` command to use instead; after each step, tell the session about
235
+ its jobs that ended.
236
+ - Blocked: `python train*.py` / `finetune*` / `pretrain*` / `*_train.py` /
237
+ `python -m pkg.train`, `torchrun`, `accelerate launch`, `deepspeed`,
238
+ `python -m torch.distributed.*`, and `CUDA_VISIBLE_DEVICES=… python …`, also
239
+ behind `uv run`, `conda run`, `nohup` and `&&` chains.
240
+ - Never blocked: commands through `gpushift`, `python -c`, `--help`, pytest, pip,
241
+ editors, and text that only mentions a command (commit messages, echo, grep).
242
+ - Escape hatches: append `# no-queue` (or prefix `GPUSHIFT_NO_QUEUE=1`); regexes in
243
+ `[hook] allow` / `deny`; `[hook] enabled = false`. It's a guardrail, not a
244
+ security boundary.
245
+
246
+ Not using the plugin? Add this to your `CLAUDE.md`:
247
+
248
+ > Never launch GPU jobs directly; submit them with `gpushift submit` (`gpushift submit --help`, `gpushift status`).
249
+
250
+ ## Making jobs resumable: `gpushift.checkpoint`
251
+
252
+ A dependency-free helper. Every gpushift job can import it, even in an environment
253
+ where gpushift isn't installed.
254
+
255
+ ```python
256
+ from gpushift import checkpoint as ckpt
257
+
258
+ last = ckpt.latest_checkpoint("checkpoints", "step*.pt") # None on a fresh start
259
+ step = load(last) if last else 0
260
+ while step < total:
261
+ train_step()
262
+ step += 1
263
+ if step % 1000 == 0 or ckpt.should_stop():
264
+ with ckpt.atomic_path(f"checkpoints/step{step:08d}.pt") as tmp:
265
+ torch.save(state, tmp) # never leaves a half-written checkpoint
266
+ if ckpt.should_stop():
267
+ break # exit 0 -> gpushift requeues and resumes it
268
+ ```
269
+
270
+ - `should_stop()`: call it every step (it's cheap). The first call also marks the job
271
+ as resumable, so `free` evicts it instead of freezing it.
272
+ - `resuming()`: True when gpushift restarted the job after an eviction (`GPUSHIFT_RESUME=1`).
273
+ - `latest_checkpoint(dir, pattern)` and `atomic_path(path)`.
274
+
275
+ Examples: [plain PyTorch](examples/pytorch_resumable.py),
276
+ [HF Trainer + accelerate](examples/hf_trainer_resumable.py),
277
+ [Lightning](examples/lightning_resumable.py). For multi-process jobs, reduce the flag
278
+ across ranks so they all stop at the same step. Scripts that handle SIGTERM
279
+ themselves can be submitted with `--resumable` (Linux/macOS).
280
+
281
+ ## Configuration
282
+
283
+ `~/.config/gpushift/config.toml` (`gpushift config --edit`). Everything is optional;
284
+ `gpushift init` writes a commented template with the values for your machine. The
285
+ daemon picks up changes automatically.
286
+
287
+ ```toml
288
+ gpus = [0, 1] # GPUs gpushift may use (default: all detected)
289
+ slots = 4 # default: 2 per GPU (CPU-only: half the cores)
290
+ shared_gpu = 1 # GPU for --light jobs (default: the last GPU)
291
+ gpu_free_perc = 75 # an unclaimed GPU is free if this % of its memory is free
292
+ starvation_guard = true
293
+ fair_share = false # true: sessions take turns
294
+ packing = true # jobs with a known memory need may share a GPU
295
+ reorder_slack = 3600 # seconds a job may be passed to cut total waiting (0 = off)
296
+ free_mode_default = "full" # or "soft"
297
+ evict_timeout = 300 # seconds a resumable job gets to checkpoint during `free`
298
+ ui_port = 8237 # dashboard on 127.0.0.1 (0 = off)
299
+
300
+ [notify]
301
+ desktop = "failures" # "all", "failures" or "off"
302
+
303
+ [auto_free]
304
+ enabled = true
305
+ apps = ["blender.exe"] # always count these programs
306
+ min_vram_mb = 1024 # other programs count above this much GPU memory
307
+ after = 10 # seconds before freeing
308
+ resume_after = 120 # seconds after the last one closes before resuming
309
+
310
+ [hook]
311
+ enabled = true
312
+ allow = [] # regexes never blocked
313
+ deny = [] # regexes always blocked
314
+ ```
315
+
316
+ - **1 GPU**: `--two` and `--exclusive` both mean "the GPU", and light jobs share it.
317
+ - **4 GPUs**: 8 slots, `--two` takes all 4 GPUs (use `--gpus 2` for a pair), GPU 3 is shared.
318
+ - **No GPU**: CPU-only mode; use `--cpu` (and `--exclusive`).
319
+
320
+ ## How it works
321
+
322
+ ```text
323
+ agents / terminal ── gpushift CLI ──┐ ┌── runner ── job (GPU 0)
324
+ ├─ SQLite ───┤
325
+ gpushift daemon (1 per user) ───────┘ queue └── runner ── job (GPU 1)
326
+ ```
327
+
328
+ - **CLI**: writes jobs to a SQLite queue (WAL). `status`, `cancel`, `free` and `resume`
329
+ work directly on it, even while the daemon is down.
330
+ - **Daemon**: once a second it decides what may start, starts a **runner** per job,
331
+ recovers runners lost to a crash or reboot, measures each job's GPU memory, watches
332
+ for your own GPU programs, sends notifications and serves the dashboard. It starts at
333
+ login: HKCU Run key on Windows, a systemd user service on Linux (run
334
+ `loginctl enable-linger` to keep it alive after logout), a LaunchAgent on macOS.
335
+ - **Runner**: one per job. It sets the environment, streams output to the job log and
336
+ records the exit code, even if the daemon restarts meanwhile.
337
+ - **GPU memory per process** comes from Windows' GPU performance counters (what Task
338
+ Manager shows) or `nvidia-smi` on Linux. **Freezing** suspends the whole process tree.
339
+
340
+ State lives in `%LOCALAPPDATA%\gpushift` (Windows), `~/.local/state/gpushift` (Linux) or
341
+ `~/Library/Application Support/gpushift` (macOS); `GPUSHIFT_HOME` overrides it.
342
+
343
+ ## Development
344
+
345
+ ```bash
346
+ python -m venv .venv && .venv/bin/pip install -e ".[dev]" # Windows: .venv\Scripts\pip
347
+ pytest
348
+ ruff check . && ruff format --check .
349
+ ```
350
+
351
+ The integration tests run a real daemon on fake GPUs (`GPUSHIFT_FAKE_GPUS=8192,6144`,
352
+ `GPUSHIFT_FAKE_GPU_PROCS` for per-process memory) with real processes, covering
353
+ free/evict/resume, auto free, packing, notifications, the dashboard, crashes while
354
+ frozen, reboots in free mode and the starvation scenario. They need no GPU.