trainq 0.1.0__py3-none-any.whl
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.
- trainq/__init__.py +0 -0
- trainq/checkpoint_watcher.py +34 -0
- trainq/client.py +383 -0
- trainq/config.py +21 -0
- trainq/executor.py +379 -0
- trainq/gpu_monitor.py +231 -0
- trainq/progress.py +292 -0
- trainq/scheduler.py +219 -0
- trainq/schema.py +78 -0
- trainq/server.py +145 -0
- trainq/watcher/__init__.py +22 -0
- trainq/watcher/checkpoint_tracker.py +82 -0
- trainq/watcher/daemon.py +177 -0
- trainq/watcher/hf_uploader.py +132 -0
- trainq-0.1.0.dist-info/METADATA +299 -0
- trainq-0.1.0.dist-info/RECORD +20 -0
- trainq-0.1.0.dist-info/WHEEL +5 -0
- trainq-0.1.0.dist-info/entry_points.txt +2 -0
- trainq-0.1.0.dist-info/licenses/LICENSE +21 -0
- trainq-0.1.0.dist-info/top_level.txt +1 -0
trainq/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Checkpoint watcher CLI entry point.
|
|
2
|
+
|
|
3
|
+
python -m trainq.checkpoint_watcher --ckpt-dir DIR [--on-checkpoint CMD] [--hf-repo-id REPO]
|
|
4
|
+
|
|
5
|
+
The implementation lives in the trainq.watcher package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from trainq.watcher import (
|
|
9
|
+
WatchConfig,
|
|
10
|
+
checkpoint_name,
|
|
11
|
+
extract_step,
|
|
12
|
+
find_checkpoints,
|
|
13
|
+
load_state,
|
|
14
|
+
save_state,
|
|
15
|
+
upload_checkpoint,
|
|
16
|
+
watch,
|
|
17
|
+
)
|
|
18
|
+
from trainq.watcher.daemon import main
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"WatchConfig",
|
|
22
|
+
"watch",
|
|
23
|
+
"find_checkpoints",
|
|
24
|
+
"checkpoint_name",
|
|
25
|
+
"extract_step",
|
|
26
|
+
"load_state",
|
|
27
|
+
"save_state",
|
|
28
|
+
"upload_checkpoint",
|
|
29
|
+
"main",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
trainq/client.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Job queue client CLI.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m trainq.client submit --command "python train.py ..." \
|
|
5
|
+
--ckpt-dir /data/ckpt --log-path /data/log.txt \
|
|
6
|
+
--gpu-count 2 --priority 3
|
|
7
|
+
|
|
8
|
+
python -m trainq.client list
|
|
9
|
+
python -m trainq.client status <job_id>
|
|
10
|
+
python -m trainq.client cancel <job_id>
|
|
11
|
+
python -m trainq.client clear
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import getpass
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
import redis
|
|
21
|
+
|
|
22
|
+
from trainq.config import (
|
|
23
|
+
REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_PASSWORD,
|
|
24
|
+
QUEUE_KEY, JOB_DETAIL_KEY, RUNNING_KEY,
|
|
25
|
+
)
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
|
|
28
|
+
from trainq.progress import parse_training_progress
|
|
29
|
+
from trainq.schema import Job, JobStatus
|
|
30
|
+
|
|
31
|
+
AGING_HOURS_PER_POINT = 30.0 # -1 per 30h waited (matches scheduler.py)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _redis_client() -> redis.Redis:
|
|
35
|
+
return redis.Redis(
|
|
36
|
+
host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB,
|
|
37
|
+
password=REDIS_PASSWORD, decode_responses=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Commands ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def cmd_submit(args: argparse.Namespace) -> None:
|
|
44
|
+
r = _redis_client()
|
|
45
|
+
|
|
46
|
+
# warn about a missing distributed launcher for multi-GPU (torchrun/accelerate/deepspeed/etc.)
|
|
47
|
+
_LAUNCHERS = ("torchrun", "accelerate", "deepspeed", "mpirun", "torch.distributed")
|
|
48
|
+
if args.gpu_count >= 2 and not any(l in args.command for l in _LAUNCHERS):
|
|
49
|
+
print(" ⚠️ gpu-count >= 2 but no distributed launcher found in the command.")
|
|
50
|
+
print(" Multi-GPU usually needs torchrun --nproc_per_node=N (or accelerate/deepspeed).")
|
|
51
|
+
print(" e.g. torchrun --nproc_per_node=2 train.py ...")
|
|
52
|
+
print(" Add --force to submit anyway. Aborting.")
|
|
53
|
+
if not getattr(args, 'force', False):
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
gpu_ids = None
|
|
57
|
+
if args.gpu_ids:
|
|
58
|
+
gpu_ids = [int(x.strip()) for x in args.gpu_ids.split(",")]
|
|
59
|
+
|
|
60
|
+
if not args.description.strip():
|
|
61
|
+
print(" ❌ --description is required. Briefly describe the job.")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
job = Job(
|
|
65
|
+
command=args.command,
|
|
66
|
+
ckpt_dir=args.ckpt_dir,
|
|
67
|
+
log_path=args.log_path,
|
|
68
|
+
description=args.description.strip(),
|
|
69
|
+
gpu_count=args.gpu_count,
|
|
70
|
+
gpu_ids=gpu_ids,
|
|
71
|
+
priority=args.priority,
|
|
72
|
+
max_retries=args.max_retries,
|
|
73
|
+
estimated_hours=args.estimated_hours,
|
|
74
|
+
estimated_vram_mb=args.estimated_vram_mb,
|
|
75
|
+
checkpoint_glob=args.checkpoint_glob,
|
|
76
|
+
on_checkpoint=args.on_checkpoint,
|
|
77
|
+
hf_repo_id=args.hf_repo_id,
|
|
78
|
+
watch_gpu=args.watch_gpu,
|
|
79
|
+
on_complete=args.on_complete,
|
|
80
|
+
user=getpass.getuser(),
|
|
81
|
+
)
|
|
82
|
+
r.set(f"{JOB_DETAIL_KEY}:{job.job_id}", job.to_json())
|
|
83
|
+
r.zadd(QUEUE_KEY, {job.job_id: job.priority})
|
|
84
|
+
parts = [f"Submitted: {job.job_id} priority={job.priority} gpu={args.gpu_count}"]
|
|
85
|
+
if args.estimated_hours > 0:
|
|
86
|
+
parts.append(f"est={args.estimated_hours}h")
|
|
87
|
+
if args.estimated_vram_mb > 0:
|
|
88
|
+
parts.append(f"vram={args.estimated_vram_mb}MB")
|
|
89
|
+
if args.hf_repo_id:
|
|
90
|
+
parts.append(f"hf={args.hf_repo_id}")
|
|
91
|
+
if args.on_checkpoint:
|
|
92
|
+
parts.append("hook=on_checkpoint")
|
|
93
|
+
print(" ".join(parts))
|
|
94
|
+
if args.estimated_vram_mb == 0:
|
|
95
|
+
print(" ⚠️ estimated-vram-mb not set → only a fully idle GPU is used (may wait longer)")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_list(args: argparse.Namespace) -> None:
|
|
99
|
+
r = _redis_client()
|
|
100
|
+
|
|
101
|
+
# pending (sorted set)
|
|
102
|
+
pending_ids = r.zrange(QUEUE_KEY, 0, -1, withscores=True)
|
|
103
|
+
running_ids = r.smembers(RUNNING_KEY)
|
|
104
|
+
|
|
105
|
+
# collect all job keys
|
|
106
|
+
all_keys = r.keys(f"{JOB_DETAIL_KEY}:*")
|
|
107
|
+
jobs: list[Job] = []
|
|
108
|
+
for k in all_keys:
|
|
109
|
+
raw = r.get(k)
|
|
110
|
+
if raw:
|
|
111
|
+
jobs.append(Job.from_json(raw))
|
|
112
|
+
|
|
113
|
+
if not jobs:
|
|
114
|
+
print("No jobs.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# effective priority — frozen at started_at/finished_at once set.
|
|
118
|
+
# while running: wait fixed by started_at. When finished (completed/failed/
|
|
119
|
+
# cancelled): allow finished_at as the snapshot end in case it ended without started_at.
|
|
120
|
+
def _wait_hours(j: Job) -> float:
|
|
121
|
+
if j.started_at is not None:
|
|
122
|
+
return (
|
|
123
|
+
datetime.fromisoformat(j.started_at) - datetime.fromisoformat(j.created_at)
|
|
124
|
+
).total_seconds() / 3600
|
|
125
|
+
if j.finished_at is not None:
|
|
126
|
+
return (
|
|
127
|
+
datetime.fromisoformat(j.finished_at) - datetime.fromisoformat(j.created_at)
|
|
128
|
+
).total_seconds() / 3600
|
|
129
|
+
return (datetime.now(timezone.utc) - datetime.fromisoformat(j.created_at)).total_seconds() / 3600
|
|
130
|
+
|
|
131
|
+
def eff_pri(j: Job) -> float:
|
|
132
|
+
return j.priority - (_wait_hours(j) / AGING_HOURS_PER_POINT)
|
|
133
|
+
|
|
134
|
+
def wait_h(j: Job) -> float:
|
|
135
|
+
return _wait_hours(j)
|
|
136
|
+
|
|
137
|
+
def elapsed_str(j: Job) -> str:
|
|
138
|
+
"""Elapsed time. Up to now while running, frozen at finished_at once ended."""
|
|
139
|
+
if j.started_at is None:
|
|
140
|
+
return "-"
|
|
141
|
+
end = (
|
|
142
|
+
datetime.fromisoformat(j.finished_at)
|
|
143
|
+
if j.finished_at is not None
|
|
144
|
+
else datetime.now(timezone.utc)
|
|
145
|
+
)
|
|
146
|
+
elapsed = (end - datetime.fromisoformat(j.started_at)).total_seconds() / 3600
|
|
147
|
+
if elapsed < 1:
|
|
148
|
+
return f"{elapsed * 60:.0f}m"
|
|
149
|
+
return f"{elapsed:.1f}h"
|
|
150
|
+
|
|
151
|
+
def progress_str(j: Job) -> str:
|
|
152
|
+
"""Log-parsed progress + ETA."""
|
|
153
|
+
if j.status != JobStatus.RUNNING or j.started_at is None:
|
|
154
|
+
return "-"
|
|
155
|
+
elapsed = (datetime.now(timezone.utc) - datetime.fromisoformat(j.started_at)).total_seconds() / 3600
|
|
156
|
+
prog = parse_training_progress(j.log_path, j.command, j.ckpt_dir)
|
|
157
|
+
if prog is not None and prog.progress > 0:
|
|
158
|
+
eta = prog.estimate_remaining_hours(elapsed)
|
|
159
|
+
return f"{prog.progress:.0%} {eta:.1f}h"
|
|
160
|
+
# fallback: estimated_hours based
|
|
161
|
+
if j.estimated_hours > 0:
|
|
162
|
+
rem = max(0, j.estimated_hours - elapsed)
|
|
163
|
+
return f"~{rem:.1f}h"
|
|
164
|
+
return "?"
|
|
165
|
+
|
|
166
|
+
# sort by status
|
|
167
|
+
order = {JobStatus.RUNNING: 0, JobStatus.PENDING: 1, JobStatus.FAILED: 2, JobStatus.COMPLETED: 3, JobStatus.CANCELLED: 4}
|
|
168
|
+
jobs.sort(key=lambda j: (order.get(j.status, 9), eff_pri(j)))
|
|
169
|
+
|
|
170
|
+
def gpu_idx(j):
|
|
171
|
+
# actually assigned GPUs (running) first, else the allow-set (gpu_ids), else all ("all")
|
|
172
|
+
if j.assigned_gpus:
|
|
173
|
+
return ",".join(str(g) for g in j.assigned_gpus)
|
|
174
|
+
if j.gpu_ids:
|
|
175
|
+
return ",".join(str(g) for g in j.gpu_ids)
|
|
176
|
+
return "all"
|
|
177
|
+
|
|
178
|
+
DESC_WIDTH = 60
|
|
179
|
+
fmt = "{:<14} {:<10} {:>5} {:>4} {:>8} {:>6} {:>5} {:>6} {:>8} {:<9} {}"
|
|
180
|
+
print(fmt.format("JOB_ID", "STATUS", "EFF", "GPUs", "GPUIDX", "VRAM", "WAIT", "ELAPS", "ETA", "USER", "DESC"))
|
|
181
|
+
print("-" * (87 + DESC_WIDTH))
|
|
182
|
+
for j in jobs:
|
|
183
|
+
ep = f"{eff_pri(j):.1f}"
|
|
184
|
+
wh = f"{wait_h(j):.1f}h"
|
|
185
|
+
vram = f"{j.estimated_vram_mb/1024:.0f}GB" if j.estimated_vram_mb > 0 else "-"
|
|
186
|
+
if j.status == JobStatus.RUNNING:
|
|
187
|
+
elaps = elapsed_str(j)
|
|
188
|
+
eta = progress_str(j)
|
|
189
|
+
elif j.status == JobStatus.COMPLETED:
|
|
190
|
+
elaps = elapsed_str(j)
|
|
191
|
+
eta = "-"
|
|
192
|
+
else: # pending, failed, cancelled
|
|
193
|
+
elaps = "-"
|
|
194
|
+
eta = f"{j.estimated_hours:.1f}h" if j.estimated_hours > 0 else "-"
|
|
195
|
+
desc = j.description[:DESC_WIDTH]
|
|
196
|
+
print(fmt.format(j.job_id, j.status.value, ep, j.gpu_count, gpu_idx(j), vram, wh, elaps, eta, (j.user or "-"), desc))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
200
|
+
r = _redis_client()
|
|
201
|
+
raw = r.get(f"{JOB_DETAIL_KEY}:{args.job_id}")
|
|
202
|
+
if raw is None:
|
|
203
|
+
print(f"Job {args.job_id} not found.")
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
job = Job.from_json(raw)
|
|
206
|
+
for field_name in [
|
|
207
|
+
"job_id", "status", "description", "command", "gpu_count", "gpu_ids", "assigned_gpus",
|
|
208
|
+
"priority", "estimated_hours", "estimated_vram_mb", "max_retries", "retry_count",
|
|
209
|
+
"ckpt_dir", "log_path",
|
|
210
|
+
"created_at", "started_at", "finished_at", "error_msg",
|
|
211
|
+
]:
|
|
212
|
+
val = getattr(job, field_name)
|
|
213
|
+
if isinstance(val, JobStatus):
|
|
214
|
+
val = val.value
|
|
215
|
+
print(f" {field_name:16s}: {val}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cmd_cancel(args: argparse.Namespace) -> None:
|
|
219
|
+
r = _redis_client()
|
|
220
|
+
raw = r.get(f"{JOB_DETAIL_KEY}:{args.job_id}")
|
|
221
|
+
if raw is None:
|
|
222
|
+
print(f"Job {args.job_id} not found.")
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
job = Job.from_json(raw)
|
|
225
|
+
if job.status != JobStatus.PENDING:
|
|
226
|
+
print(f"Cannot cancel: status is {job.status.value}")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
job.status = JobStatus.CANCELLED
|
|
229
|
+
r.set(f"{JOB_DETAIL_KEY}:{job.job_id}", job.to_json())
|
|
230
|
+
r.zrem(QUEUE_KEY, job.job_id)
|
|
231
|
+
print(f"Cancelled: {job.job_id}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def cmd_clear(args: argparse.Namespace) -> None:
|
|
235
|
+
r = _redis_client()
|
|
236
|
+
all_keys = r.keys(f"{JOB_DETAIL_KEY}:*")
|
|
237
|
+
removed = 0
|
|
238
|
+
for k in all_keys:
|
|
239
|
+
raw = r.get(k)
|
|
240
|
+
if raw:
|
|
241
|
+
job = Job.from_json(raw)
|
|
242
|
+
if job.status in (JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED):
|
|
243
|
+
r.delete(k)
|
|
244
|
+
removed += 1
|
|
245
|
+
print(f"Cleared {removed} finished jobs.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def cmd_priority(args: argparse.Namespace) -> None:
|
|
249
|
+
r = _redis_client()
|
|
250
|
+
raw = r.get(f"{JOB_DETAIL_KEY}:{args.job_id}")
|
|
251
|
+
if raw is None:
|
|
252
|
+
print(f"Job {args.job_id} not found.")
|
|
253
|
+
sys.exit(1)
|
|
254
|
+
job = Job.from_json(raw)
|
|
255
|
+
if job.status != JobStatus.PENDING:
|
|
256
|
+
print(f"Cannot change priority: status is {job.status.value} (only pending can be changed)")
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
old = job.priority
|
|
259
|
+
job.priority = args.priority
|
|
260
|
+
r.set(f"{JOB_DETAIL_KEY}:{job.job_id}", job.to_json())
|
|
261
|
+
r.zadd(QUEUE_KEY, {job.job_id: args.priority})
|
|
262
|
+
print(f"Priority updated: {job.job_id} {old} -> {args.priority}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ── Main ──────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
USAGE = """\
|
|
268
|
+
trainq — GPU job queue client
|
|
269
|
+
|
|
270
|
+
Usage:
|
|
271
|
+
trainq list list all jobs
|
|
272
|
+
trainq status <job_id> show job details
|
|
273
|
+
trainq submit --command "..." ... submit a job
|
|
274
|
+
trainq cancel <job_id> cancel a pending job
|
|
275
|
+
trainq priority <job_id> <val> change a pending job's priority (float ok; lower runs first)
|
|
276
|
+
trainq clear remove completed/failed/cancelled jobs
|
|
277
|
+
|
|
278
|
+
Minimal submit:
|
|
279
|
+
trainq submit --command "..." --ckpt-dir <dir> --log-path <path> --gpu-count 1 \\
|
|
280
|
+
--description "resnet50 finetune lr1e-4"
|
|
281
|
+
|
|
282
|
+
Recommended submit (enables backfill + GPU sharing):
|
|
283
|
+
trainq submit --command "..." --ckpt-dir <dir> --log-path <path> \\
|
|
284
|
+
--gpu-count 1 --estimated-hours 2.0 --estimated-vram-mb 15000 \\
|
|
285
|
+
--description "eval sweep seed=0"
|
|
286
|
+
|
|
287
|
+
⚠️ estimated-vram-mb not set → only a fully idle GPU is used (may wait longer)
|
|
288
|
+
⚠️ gpu-count >= 2 → the command needs a distributed launcher (torchrun, etc.)
|
|
289
|
+
⚠️ --gpu-ids is an allow-set (not a pin) → trainq picks --gpu-count from it.
|
|
290
|
+
e.g. --gpu-ids 2,3 --gpu-count 1 = GPU 2 or 3, whichever has VRAM room (not both).
|
|
291
|
+
To restrict, pass it per submit. There is no global setting.
|
|
292
|
+
|
|
293
|
+
Checkpoint hooks (checkpoint watcher):
|
|
294
|
+
trainq submit ... --checkpoint-glob 'checkpoint-*' \\
|
|
295
|
+
--on-checkpoint 'python eval.py {checkpoint}' --watch-gpu 2
|
|
296
|
+
trainq submit ... --hf-repo-id <user>/<repo> # auto-back up each new checkpoint to the HF Hub
|
|
297
|
+
|
|
298
|
+
Live ETA (the ETA column in `list`: progress + remaining time):
|
|
299
|
+
Progress is read from logs/files. Auto-detected formats: fairseq2, HF Trainer,
|
|
300
|
+
PaddleOCR(epoch), metrics.jsonl(iter), 'step X/Y'. If none match, it estimates from --estimated-hours ('~Xh').
|
|
301
|
+
★For format-agnostic real-time ETA: have the training script periodically write
|
|
302
|
+
{"step": <current>, "total": <total>} to {--ckpt-dir}/progress.json (buffering-independent, most robust).
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def main() -> None:
|
|
307
|
+
parser = argparse.ArgumentParser(
|
|
308
|
+
prog="trainq",
|
|
309
|
+
description="GPU job queue client",
|
|
310
|
+
usage=USAGE,
|
|
311
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
312
|
+
)
|
|
313
|
+
sub = parser.add_subparsers(dest="action")
|
|
314
|
+
|
|
315
|
+
# print usage when run without arguments
|
|
316
|
+
if len(sys.argv) == 1:
|
|
317
|
+
print(USAGE)
|
|
318
|
+
sys.exit(0)
|
|
319
|
+
|
|
320
|
+
# submit
|
|
321
|
+
p_sub = sub.add_parser("submit", help="Submit a job")
|
|
322
|
+
p_sub.add_argument("--command", required=True, help="Command to execute")
|
|
323
|
+
p_sub.add_argument("--ckpt-dir", required=True, help="Checkpoint / output root directory")
|
|
324
|
+
p_sub.add_argument("--log-path", required=True, help="Log file path")
|
|
325
|
+
p_sub.add_argument("--description", required=True, help="Job description, shown in `trainq list`")
|
|
326
|
+
p_sub.add_argument("--gpu-count", type=int, default=1, help="Number of GPUs needed (default: 1)")
|
|
327
|
+
p_sub.add_argument("--gpu-ids", default=None, help="Allowed GPU set, NOT forced: --gpu-count picks that many from it (VRAM-aware). Comma-separated, e.g. '2,3' with --gpu-count 1 uses GPU 2 or 3, whichever has room. No global setting; pass per submit to restrict")
|
|
328
|
+
p_sub.add_argument(
|
|
329
|
+
"--priority", type=int, default=5,
|
|
330
|
+
help=(
|
|
331
|
+
"Priority — 0 (highest) .. 9 (lowest) at submit, default: 5. "
|
|
332
|
+
"The effective priority drops below 0 (negative) via aging "
|
|
333
|
+
"(-1 per 30h waited, starvation guard — gentle so explicit priorities stay meaningful)."
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
p_sub.add_argument("--max-retries", type=int, default=1, help="Max retry count on failure (default: 1)")
|
|
337
|
+
p_sub.add_argument("--estimated-hours", type=float, default=0.0, help="Estimated time in hours (optional, 0=unknown → backfill disabled)")
|
|
338
|
+
p_sub.add_argument("--estimated-vram-mb", type=int, default=0, help="Estimated VRAM per GPU in MB (optional, 0=require fully free GPU)")
|
|
339
|
+
p_sub.add_argument("--checkpoint-glob", default=None,
|
|
340
|
+
help="Glob for new checkpoints (under ckpt_dir), e.g. 'checkpoint-*', 'ws_*/checkpoints/step_*'")
|
|
341
|
+
p_sub.add_argument("--on-checkpoint", default=None,
|
|
342
|
+
help="Shell command per new checkpoint ({checkpoint} {name} {step} {ckpt_dir} substituted)")
|
|
343
|
+
p_sub.add_argument("--hf-repo-id", default=None,
|
|
344
|
+
help="If set, auto-upload each new checkpoint to this HF Hub repo (requires HF_TOKEN)")
|
|
345
|
+
p_sub.add_argument("--watch-gpu", type=int, default=None,
|
|
346
|
+
help="Dedicated GPU for the on_checkpoint command (separate from job GPUs)")
|
|
347
|
+
p_sub.add_argument("--on-complete", default=None,
|
|
348
|
+
help="Shell command after the job ends ({ckpt_dir} {log_path} substituted, env TRAINQ_JOB_STATUS)")
|
|
349
|
+
p_sub.add_argument("--force", action="store_true",
|
|
350
|
+
help="Skip validation checks and submit anyway")
|
|
351
|
+
|
|
352
|
+
# list
|
|
353
|
+
sub.add_parser("list", help="List all jobs")
|
|
354
|
+
|
|
355
|
+
# status
|
|
356
|
+
p_st = sub.add_parser("status", help="Show job details")
|
|
357
|
+
p_st.add_argument("job_id")
|
|
358
|
+
|
|
359
|
+
# cancel
|
|
360
|
+
p_ca = sub.add_parser("cancel", help="Cancel a pending job")
|
|
361
|
+
p_ca.add_argument("job_id")
|
|
362
|
+
|
|
363
|
+
# clear
|
|
364
|
+
sub.add_parser("clear", help="Remove completed/failed/cancelled jobs")
|
|
365
|
+
|
|
366
|
+
# priority
|
|
367
|
+
p_pri = sub.add_parser("priority", help="Change a pending job's priority")
|
|
368
|
+
p_pri.add_argument("job_id")
|
|
369
|
+
p_pri.add_argument("priority", type=float, help="New priority (float ok, e.g. 5.5). Lower runs first")
|
|
370
|
+
|
|
371
|
+
args = parser.parse_args()
|
|
372
|
+
{
|
|
373
|
+
"submit": cmd_submit,
|
|
374
|
+
"list": cmd_list,
|
|
375
|
+
"status": cmd_status,
|
|
376
|
+
"cancel": cmd_cancel,
|
|
377
|
+
"clear": cmd_clear,
|
|
378
|
+
"priority": cmd_priority,
|
|
379
|
+
}[args.action](args)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
main()
|
trainq/config.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Queue system configuration constants."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# Redis
|
|
6
|
+
REDIS_HOST = os.getenv("TRAINQ_REDIS_HOST", "localhost")
|
|
7
|
+
REDIS_PORT = int(os.getenv("TRAINQ_REDIS_PORT", "6379"))
|
|
8
|
+
REDIS_DB = int(os.getenv("TRAINQ_REDIS_DB", "0"))
|
|
9
|
+
REDIS_PASSWORD = os.getenv("TRAINQ_REDIS_PASSWORD", None)
|
|
10
|
+
|
|
11
|
+
# Redis key prefix
|
|
12
|
+
KEY_PREFIX = "trainq"
|
|
13
|
+
QUEUE_KEY = f"{KEY_PREFIX}:jobs" # sorted set (priority)
|
|
14
|
+
JOB_DETAIL_KEY = f"{KEY_PREFIX}:job" # hash per job → {KEY_PREFIX}:job:{job_id}
|
|
15
|
+
RUNNING_KEY = f"{KEY_PREFIX}:running" # set of running job_ids
|
|
16
|
+
|
|
17
|
+
# Server polling
|
|
18
|
+
POLL_INTERVAL_SEC = int(os.getenv("TRAINQ_POLL_SEC", "10"))
|
|
19
|
+
|
|
20
|
+
# GPU
|
|
21
|
+
GPU_VRAM_FREE_THRESHOLD_MB = int(os.getenv("TRAINQ_VRAM_THRESHOLD_MB", "1000"))
|