gridlinegpu 0.1.9__tar.gz → 0.2.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 (73) hide show
  1. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/PKG-INFO +232 -8
  2. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/README.md +231 -7
  3. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/pyproject.toml +1 -1
  4. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/__init__.py +9 -2
  5. gridlinegpu-0.2.0/src/gridlinegpu/_app_entry.py +114 -0
  6. gridlinegpu-0.2.0/src/gridlinegpu/_app_remote.py +175 -0
  7. gridlinegpu-0.2.0/src/gridlinegpu/_batch.py +197 -0
  8. gridlinegpu-0.2.0/src/gridlinegpu/_finetune_job.py +121 -0
  9. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_capture.py +3 -1
  10. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_contract.py +10 -1
  11. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_errors.py +127 -2
  12. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_interfaces.py +71 -5
  13. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_job.py +41 -10
  14. gridlinegpu-0.2.0/src/gridlinegpu/_source_pool.py +611 -0
  15. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_public.py +49 -0
  16. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_release.py +1 -1
  17. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_run.py +12 -1
  18. gridlinegpu-0.2.0/src/gridlinegpu/app.py +770 -0
  19. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/cli.py +7 -1
  20. gridlinegpu-0.2.0/src/gridlinegpu/cli_app.py +218 -0
  21. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/client.py +79 -1
  22. gridlinegpu-0.2.0/src/gridlinegpu/finetune.py +78 -0
  23. gridlinegpu-0.2.0/src/gridlinegpu/worker.py +208 -0
  24. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu.egg-info/PKG-INFO +232 -8
  25. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu.egg-info/SOURCES.txt +17 -1
  26. gridlinegpu-0.2.0/tests/test_app.py +349 -0
  27. gridlinegpu-0.2.0/tests/test_app_map.py +360 -0
  28. gridlinegpu-0.2.0/tests/test_batch.py +248 -0
  29. gridlinegpu-0.2.0/tests/test_cli_app.py +167 -0
  30. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_client_source.py +1 -1
  31. gridlinegpu-0.2.0/tests/test_finetune_helper.py +101 -0
  32. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_contract.py +78 -3
  33. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_errors.py +8 -3
  34. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_job.py +22 -7
  35. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_job_results.py +2 -2
  36. gridlinegpu-0.2.0/tests/test_source_pool.py +435 -0
  37. gridlinegpu-0.2.0/tests/test_worker.py +161 -0
  38. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/setup.cfg +0 -0
  39. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/UNICODE-LICENSE.txt +0 -0
  40. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/__main__.py +0 -0
  41. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_access.py +0 -0
  42. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_admission.py +0 -0
  43. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_casefold.json +0 -0
  44. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_credits.py +0 -0
  45. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_history.py +0 -0
  46. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_outputs.py +0 -0
  47. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_results.py +0 -0
  48. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_submission.py +0 -0
  49. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/_source_transfer.py +0 -0
  50. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/cli_auth.py +0 -0
  51. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu/errors.py +0 -0
  52. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu.egg-info/dependency_links.txt +0 -0
  53. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu.egg-info/entry_points.txt +0 -0
  54. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu.egg-info/requires.txt +0 -0
  55. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/src/gridlinegpu.egg-info/top_level.txt +0 -0
  56. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_cli.py +0 -0
  57. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_cli_auth.py +0 -0
  58. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_client.py +0 -0
  59. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_finetune_acceptance.py +0 -0
  60. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_access.py +0 -0
  61. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_admission.py +0 -0
  62. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_canary.py +0 -0
  63. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_capture.py +0 -0
  64. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_capture_gaps.py +0 -0
  65. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_credits.py +0 -0
  66. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_curated_run.py +0 -0
  67. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_history.py +0 -0
  68. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_o6c_acceptance.py +0 -0
  69. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_release.py +0 -0
  70. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_results.py +0 -0
  71. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_run.py +0 -0
  72. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_submission.py +0 -0
  73. {gridlinegpu-0.1.9 → gridlinegpu-0.2.0}/tests/test_source_transfer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridlinegpu
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Python client for Gridline YAML workload routing
5
5
  Author: Gridline GPU
6
6
  License: Proprietary
@@ -28,13 +28,13 @@ Linux.
28
28
  1. **Sign up** at https://gridlinegpu.com/signup and verify your email.
29
29
  2. **Request access** at https://gridlinegpu.com/get-started: accept the partner beta terms and
30
30
  describe your use case in a few lines.
31
- 3. **Wait for approval.** Gridline approves each request by hand and grants your account beta
32
- credit; the get-started page shows the status. There is no payment step: jobs draw on that
33
- credit.
31
+ 3. **Wait for approval.** Gridline checks each request; approval grants your account beta
32
+ credit, and the get-started page shows the status. There is no payment step: jobs draw on
33
+ that credit.
34
34
  4. **Install** the SDK with the source extra (Python 3.10 or newer):
35
35
 
36
36
  ```sh
37
- python -m pip install 'gridlinegpu[source-contract]>=0.1.9,<0.2'
37
+ python -m pip install 'gridlinegpu[source-contract]>=0.2.0,<0.3'
38
38
  ```
39
39
 
40
40
  5. **Create a token** once. Browser approval saves a private, scoped token file; it is never
@@ -151,6 +151,230 @@ compiler. Without it, the YAML commands and `client.access()` still work and sou
151
151
  `source_dependencies_missing` with the install line. The "Source runs (beta)" section below has the
152
152
  limits, costs and failure codes.
153
153
 
154
+ ## Functions: `App`, `@app.function`, `.remote()` and `.spawn()` (preview)
155
+
156
+ Decorate a Python function instead of writing YAML. Each call becomes one ordinary source job,
157
+ with the same access, credit holds, limits, isolation and billing as `run()`:
158
+
159
+ ```python
160
+ import gridlinegpu as gl
161
+
162
+ app = gl.App("transcribe")
163
+ image = gl.Image.curated("gridline/hf-torch-2.5@r1")
164
+
165
+ @app.function(image=image, gpu="any:16GB", outputs={"transcript": "transcript.txt"})
166
+ def transcribe(audio: gl.File, language: str = "en") -> dict:
167
+ text = run_model(audio.path, language)
168
+ (gl.output_dir() / "transcript.txt").write_text(text)
169
+ return {"chars": len(text)}
170
+
171
+ print(transcribe.remote(gl.File("audio/a.flac"))) # waits; files land in gridline-outputs/
172
+ call = transcribe.spawn(gl.File("audio/b.flac")) # returns once the job is admitted
173
+ print(call.job_id, call.get())
174
+ ```
175
+
176
+ How the parts work:
177
+
178
+ - **Code.** The function's module and the local modules and packages it imports (read from the
179
+ source, never run) are sent with the job. Add data files with `include=["data/*.json"]`. Inside
180
+ the job, `gridlinegpu` is a small stand-in, so the module imports in any image; only the
181
+ function runs, never `local_entrypoint` or `if __name__ == "__main__"` code.
182
+ - **Arguments and return values** are JSON data plus `gl.File` arguments. There is no pickle.
183
+ Each `gl.File` is uploaded as an input file (at most 512 MiB). The return value is at most
184
+ 8 MiB. Larger results are files written under `gl.output_dir()` and declared in `outputs`
185
+ (at most 128 MiB each).
186
+ - **Options:**
187
+ - `gpu="any:<N>GB"` sets the minimum GPU memory (default 8 GB).
188
+ - `models={"base": gl.Model.hf(repo, revision)}` is fetched by the platform and found at
189
+ `gl.model_path("base")`.
190
+ - `kind="training"` marks a training job.
191
+ - `timeout=` is a local limit, enforced best-effort while the SDK waits; past it the job is
192
+ cancelled.
193
+ - **Not yet available:** named GPU types, `max_price`, `capacity` and built images. Each
194
+ raises when used.
195
+
196
+ **Many calls: `.map()` and `.starmap()`.** These return a `MapRun`; iterating it submits the calls
197
+ and yields their values. Nothing is submitted before that.
198
+
199
+ ```python
200
+ for value in transcribe.map([gl.File(path) for path in sorted(Path("audio").glob("*.flac"))],
201
+ kwargs={"language": "en"}):
202
+ print(value)
203
+ results = list(transcribe.starmap([(gl.File("a.flac"), "en"), (gl.File("b.flac"), "de")],
204
+ max_spend_usd=5.0, return_exceptions=True))
205
+ ```
206
+
207
+ A map runs one of two ways:
208
+
209
+ - **Warm pool.** With `max_spend_usd` (and `warm=None`, the default) or `warm=True`, a worker
210
+ pool of 1 to `max_workers` (at most 4) GPU workers runs the calls. The module loads once per
211
+ worker.
212
+ - Each call is one item, delivered at least once, with exactly one result committed.
213
+ - The function's `timeout` is the item timeout (60 to 3,600 seconds).
214
+ - A pooled function declares at most 7 outputs, each a file name directly in
215
+ `gl.output_dir()`.
216
+ - An account without pools falls back to chunks, unless `warm=True`.
217
+ - **Chunks of ordinary jobs.** `per_job` calls per job (default 16; 1 when the function
218
+ declares outputs), at most `max_running` jobs at once (default 2, the account's
219
+ running-jobs limit). A call that raises is recorded and the job runs its next call.
220
+
221
+ Common to both:
222
+
223
+ - Values come back in input order (`order="completed"` yields each as it finishes).
224
+ - `@app.function(retries=N)` (0 to 4) gives each failed call N more attempts.
225
+ - A call that still fails raises `gl.FunctionError` (`function_raised`, the job's failure code,
226
+ or `item_dead`). With `return_exceptions=True` it is yielded instead.
227
+ - Each job's or item's files land under `gridline-outputs/<function>/<job or pool id>/`.
228
+
229
+ Inside a job, a decorated function is the plain function: call other functions with
230
+ `.local()`. `.remote()` and `.map()` are refused there, because the job has no network.
231
+
232
+ **Fine-tuning in one call.** `gl.finetune.lora()` and `gl.finetune.qlora()` submit one training job on
233
+ `gridline/hf-finetune-2.5@r1`. The job runs Gridline's recipe, trl `SFTTrainer` with a peft LoRA:
234
+ bf16 for LoRA, 4-bit NF4 for QLoRA. The platform fetches the base model at its pinned revision;
235
+ the dataset is a JSON-lines file whose rows have a `text` field:
236
+
237
+ ```python
238
+ run = gl.finetune.qlora(gl.Model.hf("HuggingFaceTB/SmolLM2-135M", revision="93efa2f097d58c2a74874c7e644dbc9b0cee75a2"),
239
+ "data/train.jsonl", eval_dataset="data/eval.jsonl", epochs=2, rank=16)
240
+ metrics = run.get(outputs_dir="adapters/smol") # adapter_model.safetensors, adapter_config.json, metrics.json
241
+ print(metrics["eval_loss_before"], metrics["eval_loss_after"])
242
+ ```
243
+
244
+ Options: `epochs`, `learning_rate`, `rank`, `alpha`, `dropout`, `max_seq_length`,
245
+ `batch_size`, `max_steps`, `target_modules` and `gpu` (default `"any:16GB"`). The adapter is at
246
+ most 128 MiB. Full fine-tunes (all weights) are not offered yet.
247
+
248
+ **From the command line.** Running submits paid jobs, so it needs `--apply`:
249
+
250
+ ```sh
251
+ gridline app show transcribe_app.py # each function's generated YAML; nothing is sent
252
+ gridline run --apply transcribe_app.py --folder audio # the local_entrypoint, arguments typed from it
253
+ gridline run --apply transcribe_app.py::transcribe --audio a.flac --language en
254
+ gridline run --apply --detach transcribe_app.py::transcribe --audio a.flac # print the job id and exit
255
+ gridline pools status <pool id> # also: items, close, cancel --apply
256
+ ```
257
+
258
+ `gridline run` imports the file the way `python app.py` would. Each `--name value` is converted
259
+ by the parameter's annotation or default: `int`, `float`, `bool`, `str`, or `gl.File` for a path.
260
+ - **Failures:** a failed job raises `gl.FunctionError` with the job's failure code. An uncertain
261
+ submission raises `SubmissionOutcomeUnknown`, as `run()` does; continue it with
262
+ `transcribe.resume(error.submission_id)`.
263
+
264
+ ## Many files: batches of ordinary jobs
265
+
266
+ `client.run_batch()` (0.1.10 or newer) runs one workload over many files, at most `per_job` files
267
+ (1 to 64) per job.
268
+ Each chunk is an ordinary `run()`, with the same admission, funding, routing, isolation and
269
+ billing as any job, and nothing new on the server. The workload declares one input per slot,
270
+ `item_00` to `item_<per_job - 1>` (`gridlinegpu.batch_slots(per_job)` lists the names). Give each
271
+ slot a default `source`, such as a small placeholder file, and its own `inputs/` path:
272
+
273
+ ```yaml
274
+ task:
275
+ # ...
276
+ command: [python, transcribe.py, --count, {parameter: count}]
277
+ inputs:
278
+ item_00: {source: audio/empty.flac, path: inputs/item_00.flac}
279
+ item_01: {source: audio/empty.flac, path: inputs/item_01.flac}
280
+ # ... through item_31 for per_job=32
281
+ parameters:
282
+ count: {type: integer, default: 0}
283
+ ```
284
+
285
+ ```python
286
+ from pathlib import Path
287
+
288
+ run = client.run_batch("transcribe.yaml", sorted(Path("audio").glob("*.flac")), per_job=32,
289
+ count_parameter="count")
290
+ for done in run.results(): # one BatchResult per job, as each one finishes
291
+ if done.succeeded:
292
+ done.job.outputs(f"results/{done.index:04d}")
293
+ else:
294
+ print(done.index, done.files, done.result.failure_code)
295
+ ```
296
+
297
+ - **Nothing is submitted until `results()` is iterated.** At most `max_running` jobs (default 2,
298
+ the per-account running-jobs default) run at once.
299
+ - **When the account is at its running-jobs limit,** the chunk waits and the same submission is
300
+ resumed once a job finishes.
301
+ - **A short last chunk** leaves its unused slots at their default. Use `count_parameter` to tell
302
+ the job how many real files it got (an `integer` parameter bound in `command`).
303
+ - **Limits:** files are project-relative paths, at most 512 MiB each and 1 GiB per job, as for any
304
+ input.
305
+ - **Cost:** every job pays its own start-up (pod start, image, model load). Choose `per_job` so
306
+ each job's work is well above that.
307
+ - **Errors:** an error other than the running-jobs limit is raised as `run()` raises it.
308
+ - `run.jobs` lists what was submitted, and `run.remaining` lists what was not.
309
+ - An error with a `submission_id` is resumed with `client.resume()`, never run again.
310
+ - **Without a client:** `gridlinegpu.batch(files, per_job)` returns the input maps alone, one per
311
+ job, to pass as `run(..., inputs=...)`.
312
+
313
+ ## Worker pools (beta)
314
+
315
+ A worker pool runs one workload over a stream of files on 1 to 4 warm GPU workers. The model
316
+ loads once per worker instead of once per file. Pools are in beta: the account must be enabled
317
+ for them (`pool_owner_not_allowed` otherwise). `client.pool()` and `gridlinegpu.worker` need
318
+ 0.1.10 or newer.
319
+
320
+ ```python
321
+ from pathlib import Path
322
+
323
+ pool = client.pool("transcribe.yaml", max_workers=2, max_spend_usd=5.0, unit_name="audio_second")
324
+ pool.put_many(sorted(Path("audio").glob("*.flac"))) # enqueue any time while the pool is open
325
+ pool.close() # no more items: workers drain, then stop
326
+ for item in pool.results(directory="results"): # each item once it is finished; done
327
+ print(item.key, item.state, item.last_error_detail) # items' outputs land in results/<key>/
328
+ ```
329
+
330
+ `pool.outputs(key, directory)` downloads one done item's outputs. Each file is checked against its
331
+ listed size and SHA-256 before it appears, and an existing identical file is kept. Outputs are
332
+ kept for 7 days after the item finished (`pool_item_output_expired` after that).
333
+
334
+ The workload's command runs once per worker and takes items with `gridlinegpu.worker`:
335
+
336
+ ```python
337
+ import gridlinegpu.worker
338
+
339
+ model = load_model()
340
+ for item in gridlinegpu.worker.items():
341
+ text = model.transcribe(item.input_path)
342
+ (item.output_dir / "transcript.json").write_text(text)
343
+ item.done() # or item.fail("why"); an exception hands the item back failed
344
+ ```
345
+
346
+ What a pool is:
347
+
348
+ - **Bounded.** A pool has hard limits: `max_duration_seconds` (default 3,600, at most 24 hours),
349
+ `max_items` (at most 5,000), `max_spend_usd` and `max_workers` (at most 4). Each worker is an
350
+ ordinary 1-GPU job with the same routing, isolation, holds and settlement as `run()`.
351
+ - **Scaling.** Gridline starts workers while queued work justifies them and stops a worker after
352
+ `idle_seconds` (default 120) without items. An idle pool costs nothing; enqueue again to
353
+ restart it.
354
+ - **Billing.** Billing is pass-through: you pay the workers' GPU time, with no pool fee.
355
+ `status()["money"]` shows what is held and settled. The per-item cost is informational: each
356
+ worker's settled cost is shared by busy time, and cold start and idle time are shown
357
+ separately.
358
+ - **Items.** Each item has one input file (at most 512 MiB). Uploads go in batches of at most 64
359
+ files and 1 GiB. An item has at most 8 output files, 128 MiB each and 256 MiB in all.
360
+ - **Retries.** A failed item is retried after a backoff, up to `max_attempts` (default 3, at
361
+ most 5), then kept as `dead` with its last error. `pool.retry(key)` requeues it once. A lost
362
+ machine requeues an item without using an attempt.
363
+ - **Keys.** A key defaults to the file's SHA-256, so the same file is enqueued once. Pass `key=`
364
+ to name items yourself.
365
+ - **At least once.** An item can run twice, for example when a worker is lost after finishing
366
+ it or a lease expires. Only one result is ever committed, so make your work idempotent.
367
+ Transcription and most batch inference already are.
368
+ - **Shared process.** Items of one pool share a warm process inside a worker. They are not
369
+ isolated from each other, only from other accounts.
370
+ - **No network.** As with `run()`, your code has no network. Items arrive as local files over a
371
+ Unix socket that Gridline's supervisor serves.
372
+
373
+ If `pool()` ends with `submission_pending` (uncertain), call `client.resume_pool(submission_id)`.
374
+ It resends the same request and returns the same pool. `client.pool_handle(pool_id)` reattaches
375
+ to a pool from another process.
376
+
377
+
154
378
  ## YAML routing workloads
155
379
 
156
380
  Submit one YAML workload through Gridline's authenticated API. The Python client stages
@@ -158,7 +382,7 @@ files referenced by YAML, returns a route for review, then launches only that ex
158
382
  fresh plan when you supply a cost ceiling and idempotency key.
159
383
 
160
384
  ```sh
161
- python -m pip install 'gridlinegpu>=0.1.9,<0.2'
385
+ python -m pip install 'gridlinegpu>=0.2.0,<0.3'
162
386
  ```
163
387
 
164
388
  Run browser setup once. It creates a 30-day scoped token in a private file, attempts to
@@ -272,7 +496,7 @@ step in the SDK. Request access first (see the quickstart above); `client.access
272
496
  `gridline access` show where your request stands.
273
497
 
274
498
  ```sh
275
- python -m pip install 'gridlinegpu[source-contract]>=0.1.9,<0.2'
499
+ python -m pip install 'gridlinegpu[source-contract]>=0.2.0,<0.3'
276
500
  gridline setup --apply # once: saves a private, scoped token file
277
501
  gridline access # "approved" before your first run
278
502
  ```
@@ -395,7 +619,7 @@ Under `hardened_user_separation` each process has an 8 GiB data-memory limit, so
395
619
  base model straight to the GPU (`device_map` or 4-bit loading) rather than building it in
396
620
  CPU memory. While a job runs, about $1.78 of credits is held per window of about an hour;
397
621
  it settles at the actual cost after teardown (the acceptance job cost about $0.06). Full
398
- fine-tunes that return merged weights need larger outputs and are coming soon.
622
+ fine-tunes that return merged weights need larger outputs and are not yet offered.
399
623
 
400
624
  The rest of this section describes how source runs behave.
401
625
 
@@ -10,13 +10,13 @@ Linux.
10
10
  1. **Sign up** at https://gridlinegpu.com/signup and verify your email.
11
11
  2. **Request access** at https://gridlinegpu.com/get-started: accept the partner beta terms and
12
12
  describe your use case in a few lines.
13
- 3. **Wait for approval.** Gridline approves each request by hand and grants your account beta
14
- credit; the get-started page shows the status. There is no payment step: jobs draw on that
15
- credit.
13
+ 3. **Wait for approval.** Gridline checks each request; approval grants your account beta
14
+ credit, and the get-started page shows the status. There is no payment step: jobs draw on
15
+ that credit.
16
16
  4. **Install** the SDK with the source extra (Python 3.10 or newer):
17
17
 
18
18
  ```sh
19
- python -m pip install 'gridlinegpu[source-contract]>=0.1.9,<0.2'
19
+ python -m pip install 'gridlinegpu[source-contract]>=0.2.0,<0.3'
20
20
  ```
21
21
 
22
22
  5. **Create a token** once. Browser approval saves a private, scoped token file; it is never
@@ -133,6 +133,230 @@ compiler. Without it, the YAML commands and `client.access()` still work and sou
133
133
  `source_dependencies_missing` with the install line. The "Source runs (beta)" section below has the
134
134
  limits, costs and failure codes.
135
135
 
136
+ ## Functions: `App`, `@app.function`, `.remote()` and `.spawn()` (preview)
137
+
138
+ Decorate a Python function instead of writing YAML. Each call becomes one ordinary source job,
139
+ with the same access, credit holds, limits, isolation and billing as `run()`:
140
+
141
+ ```python
142
+ import gridlinegpu as gl
143
+
144
+ app = gl.App("transcribe")
145
+ image = gl.Image.curated("gridline/hf-torch-2.5@r1")
146
+
147
+ @app.function(image=image, gpu="any:16GB", outputs={"transcript": "transcript.txt"})
148
+ def transcribe(audio: gl.File, language: str = "en") -> dict:
149
+ text = run_model(audio.path, language)
150
+ (gl.output_dir() / "transcript.txt").write_text(text)
151
+ return {"chars": len(text)}
152
+
153
+ print(transcribe.remote(gl.File("audio/a.flac"))) # waits; files land in gridline-outputs/
154
+ call = transcribe.spawn(gl.File("audio/b.flac")) # returns once the job is admitted
155
+ print(call.job_id, call.get())
156
+ ```
157
+
158
+ How the parts work:
159
+
160
+ - **Code.** The function's module and the local modules and packages it imports (read from the
161
+ source, never run) are sent with the job. Add data files with `include=["data/*.json"]`. Inside
162
+ the job, `gridlinegpu` is a small stand-in, so the module imports in any image; only the
163
+ function runs, never `local_entrypoint` or `if __name__ == "__main__"` code.
164
+ - **Arguments and return values** are JSON data plus `gl.File` arguments. There is no pickle.
165
+ Each `gl.File` is uploaded as an input file (at most 512 MiB). The return value is at most
166
+ 8 MiB. Larger results are files written under `gl.output_dir()` and declared in `outputs`
167
+ (at most 128 MiB each).
168
+ - **Options:**
169
+ - `gpu="any:<N>GB"` sets the minimum GPU memory (default 8 GB).
170
+ - `models={"base": gl.Model.hf(repo, revision)}` is fetched by the platform and found at
171
+ `gl.model_path("base")`.
172
+ - `kind="training"` marks a training job.
173
+ - `timeout=` is a local limit, enforced best-effort while the SDK waits; past it the job is
174
+ cancelled.
175
+ - **Not yet available:** named GPU types, `max_price`, `capacity` and built images. Each
176
+ raises when used.
177
+
178
+ **Many calls: `.map()` and `.starmap()`.** These return a `MapRun`; iterating it submits the calls
179
+ and yields their values. Nothing is submitted before that.
180
+
181
+ ```python
182
+ for value in transcribe.map([gl.File(path) for path in sorted(Path("audio").glob("*.flac"))],
183
+ kwargs={"language": "en"}):
184
+ print(value)
185
+ results = list(transcribe.starmap([(gl.File("a.flac"), "en"), (gl.File("b.flac"), "de")],
186
+ max_spend_usd=5.0, return_exceptions=True))
187
+ ```
188
+
189
+ A map runs one of two ways:
190
+
191
+ - **Warm pool.** With `max_spend_usd` (and `warm=None`, the default) or `warm=True`, a worker
192
+ pool of 1 to `max_workers` (at most 4) GPU workers runs the calls. The module loads once per
193
+ worker.
194
+ - Each call is one item, delivered at least once, with exactly one result committed.
195
+ - The function's `timeout` is the item timeout (60 to 3,600 seconds).
196
+ - A pooled function declares at most 7 outputs, each a file name directly in
197
+ `gl.output_dir()`.
198
+ - An account without pools falls back to chunks, unless `warm=True`.
199
+ - **Chunks of ordinary jobs.** `per_job` calls per job (default 16; 1 when the function
200
+ declares outputs), at most `max_running` jobs at once (default 2, the account's
201
+ running-jobs limit). A call that raises is recorded and the job runs its next call.
202
+
203
+ Common to both:
204
+
205
+ - Values come back in input order (`order="completed"` yields each as it finishes).
206
+ - `@app.function(retries=N)` (0 to 4) gives each failed call N more attempts.
207
+ - A call that still fails raises `gl.FunctionError` (`function_raised`, the job's failure code,
208
+ or `item_dead`). With `return_exceptions=True` it is yielded instead.
209
+ - Each job's or item's files land under `gridline-outputs/<function>/<job or pool id>/`.
210
+
211
+ Inside a job, a decorated function is the plain function: call other functions with
212
+ `.local()`. `.remote()` and `.map()` are refused there, because the job has no network.
213
+
214
+ **Fine-tuning in one call.** `gl.finetune.lora()` and `gl.finetune.qlora()` submit one training job on
215
+ `gridline/hf-finetune-2.5@r1`. The job runs Gridline's recipe, trl `SFTTrainer` with a peft LoRA:
216
+ bf16 for LoRA, 4-bit NF4 for QLoRA. The platform fetches the base model at its pinned revision;
217
+ the dataset is a JSON-lines file whose rows have a `text` field:
218
+
219
+ ```python
220
+ run = gl.finetune.qlora(gl.Model.hf("HuggingFaceTB/SmolLM2-135M", revision="93efa2f097d58c2a74874c7e644dbc9b0cee75a2"),
221
+ "data/train.jsonl", eval_dataset="data/eval.jsonl", epochs=2, rank=16)
222
+ metrics = run.get(outputs_dir="adapters/smol") # adapter_model.safetensors, adapter_config.json, metrics.json
223
+ print(metrics["eval_loss_before"], metrics["eval_loss_after"])
224
+ ```
225
+
226
+ Options: `epochs`, `learning_rate`, `rank`, `alpha`, `dropout`, `max_seq_length`,
227
+ `batch_size`, `max_steps`, `target_modules` and `gpu` (default `"any:16GB"`). The adapter is at
228
+ most 128 MiB. Full fine-tunes (all weights) are not offered yet.
229
+
230
+ **From the command line.** Running submits paid jobs, so it needs `--apply`:
231
+
232
+ ```sh
233
+ gridline app show transcribe_app.py # each function's generated YAML; nothing is sent
234
+ gridline run --apply transcribe_app.py --folder audio # the local_entrypoint, arguments typed from it
235
+ gridline run --apply transcribe_app.py::transcribe --audio a.flac --language en
236
+ gridline run --apply --detach transcribe_app.py::transcribe --audio a.flac # print the job id and exit
237
+ gridline pools status <pool id> # also: items, close, cancel --apply
238
+ ```
239
+
240
+ `gridline run` imports the file the way `python app.py` would. Each `--name value` is converted
241
+ by the parameter's annotation or default: `int`, `float`, `bool`, `str`, or `gl.File` for a path.
242
+ - **Failures:** a failed job raises `gl.FunctionError` with the job's failure code. An uncertain
243
+ submission raises `SubmissionOutcomeUnknown`, as `run()` does; continue it with
244
+ `transcribe.resume(error.submission_id)`.
245
+
246
+ ## Many files: batches of ordinary jobs
247
+
248
+ `client.run_batch()` (0.1.10 or newer) runs one workload over many files, at most `per_job` files
249
+ (1 to 64) per job.
250
+ Each chunk is an ordinary `run()`, with the same admission, funding, routing, isolation and
251
+ billing as any job, and nothing new on the server. The workload declares one input per slot,
252
+ `item_00` to `item_<per_job - 1>` (`gridlinegpu.batch_slots(per_job)` lists the names). Give each
253
+ slot a default `source`, such as a small placeholder file, and its own `inputs/` path:
254
+
255
+ ```yaml
256
+ task:
257
+ # ...
258
+ command: [python, transcribe.py, --count, {parameter: count}]
259
+ inputs:
260
+ item_00: {source: audio/empty.flac, path: inputs/item_00.flac}
261
+ item_01: {source: audio/empty.flac, path: inputs/item_01.flac}
262
+ # ... through item_31 for per_job=32
263
+ parameters:
264
+ count: {type: integer, default: 0}
265
+ ```
266
+
267
+ ```python
268
+ from pathlib import Path
269
+
270
+ run = client.run_batch("transcribe.yaml", sorted(Path("audio").glob("*.flac")), per_job=32,
271
+ count_parameter="count")
272
+ for done in run.results(): # one BatchResult per job, as each one finishes
273
+ if done.succeeded:
274
+ done.job.outputs(f"results/{done.index:04d}")
275
+ else:
276
+ print(done.index, done.files, done.result.failure_code)
277
+ ```
278
+
279
+ - **Nothing is submitted until `results()` is iterated.** At most `max_running` jobs (default 2,
280
+ the per-account running-jobs default) run at once.
281
+ - **When the account is at its running-jobs limit,** the chunk waits and the same submission is
282
+ resumed once a job finishes.
283
+ - **A short last chunk** leaves its unused slots at their default. Use `count_parameter` to tell
284
+ the job how many real files it got (an `integer` parameter bound in `command`).
285
+ - **Limits:** files are project-relative paths, at most 512 MiB each and 1 GiB per job, as for any
286
+ input.
287
+ - **Cost:** every job pays its own start-up (pod start, image, model load). Choose `per_job` so
288
+ each job's work is well above that.
289
+ - **Errors:** an error other than the running-jobs limit is raised as `run()` raises it.
290
+ - `run.jobs` lists what was submitted, and `run.remaining` lists what was not.
291
+ - An error with a `submission_id` is resumed with `client.resume()`, never run again.
292
+ - **Without a client:** `gridlinegpu.batch(files, per_job)` returns the input maps alone, one per
293
+ job, to pass as `run(..., inputs=...)`.
294
+
295
+ ## Worker pools (beta)
296
+
297
+ A worker pool runs one workload over a stream of files on 1 to 4 warm GPU workers. The model
298
+ loads once per worker instead of once per file. Pools are in beta: the account must be enabled
299
+ for them (`pool_owner_not_allowed` otherwise). `client.pool()` and `gridlinegpu.worker` need
300
+ 0.1.10 or newer.
301
+
302
+ ```python
303
+ from pathlib import Path
304
+
305
+ pool = client.pool("transcribe.yaml", max_workers=2, max_spend_usd=5.0, unit_name="audio_second")
306
+ pool.put_many(sorted(Path("audio").glob("*.flac"))) # enqueue any time while the pool is open
307
+ pool.close() # no more items: workers drain, then stop
308
+ for item in pool.results(directory="results"): # each item once it is finished; done
309
+ print(item.key, item.state, item.last_error_detail) # items' outputs land in results/<key>/
310
+ ```
311
+
312
+ `pool.outputs(key, directory)` downloads one done item's outputs. Each file is checked against its
313
+ listed size and SHA-256 before it appears, and an existing identical file is kept. Outputs are
314
+ kept for 7 days after the item finished (`pool_item_output_expired` after that).
315
+
316
+ The workload's command runs once per worker and takes items with `gridlinegpu.worker`:
317
+
318
+ ```python
319
+ import gridlinegpu.worker
320
+
321
+ model = load_model()
322
+ for item in gridlinegpu.worker.items():
323
+ text = model.transcribe(item.input_path)
324
+ (item.output_dir / "transcript.json").write_text(text)
325
+ item.done() # or item.fail("why"); an exception hands the item back failed
326
+ ```
327
+
328
+ What a pool is:
329
+
330
+ - **Bounded.** A pool has hard limits: `max_duration_seconds` (default 3,600, at most 24 hours),
331
+ `max_items` (at most 5,000), `max_spend_usd` and `max_workers` (at most 4). Each worker is an
332
+ ordinary 1-GPU job with the same routing, isolation, holds and settlement as `run()`.
333
+ - **Scaling.** Gridline starts workers while queued work justifies them and stops a worker after
334
+ `idle_seconds` (default 120) without items. An idle pool costs nothing; enqueue again to
335
+ restart it.
336
+ - **Billing.** Billing is pass-through: you pay the workers' GPU time, with no pool fee.
337
+ `status()["money"]` shows what is held and settled. The per-item cost is informational: each
338
+ worker's settled cost is shared by busy time, and cold start and idle time are shown
339
+ separately.
340
+ - **Items.** Each item has one input file (at most 512 MiB). Uploads go in batches of at most 64
341
+ files and 1 GiB. An item has at most 8 output files, 128 MiB each and 256 MiB in all.
342
+ - **Retries.** A failed item is retried after a backoff, up to `max_attempts` (default 3, at
343
+ most 5), then kept as `dead` with its last error. `pool.retry(key)` requeues it once. A lost
344
+ machine requeues an item without using an attempt.
345
+ - **Keys.** A key defaults to the file's SHA-256, so the same file is enqueued once. Pass `key=`
346
+ to name items yourself.
347
+ - **At least once.** An item can run twice, for example when a worker is lost after finishing
348
+ it or a lease expires. Only one result is ever committed, so make your work idempotent.
349
+ Transcription and most batch inference already are.
350
+ - **Shared process.** Items of one pool share a warm process inside a worker. They are not
351
+ isolated from each other, only from other accounts.
352
+ - **No network.** As with `run()`, your code has no network. Items arrive as local files over a
353
+ Unix socket that Gridline's supervisor serves.
354
+
355
+ If `pool()` ends with `submission_pending` (uncertain), call `client.resume_pool(submission_id)`.
356
+ It resends the same request and returns the same pool. `client.pool_handle(pool_id)` reattaches
357
+ to a pool from another process.
358
+
359
+
136
360
  ## YAML routing workloads
137
361
 
138
362
  Submit one YAML workload through Gridline's authenticated API. The Python client stages
@@ -140,7 +364,7 @@ files referenced by YAML, returns a route for review, then launches only that ex
140
364
  fresh plan when you supply a cost ceiling and idempotency key.
141
365
 
142
366
  ```sh
143
- python -m pip install 'gridlinegpu>=0.1.9,<0.2'
367
+ python -m pip install 'gridlinegpu>=0.2.0,<0.3'
144
368
  ```
145
369
 
146
370
  Run browser setup once. It creates a 30-day scoped token in a private file, attempts to
@@ -254,7 +478,7 @@ step in the SDK. Request access first (see the quickstart above); `client.access
254
478
  `gridline access` show where your request stands.
255
479
 
256
480
  ```sh
257
- python -m pip install 'gridlinegpu[source-contract]>=0.1.9,<0.2'
481
+ python -m pip install 'gridlinegpu[source-contract]>=0.2.0,<0.3'
258
482
  gridline setup --apply # once: saves a private, scoped token file
259
483
  gridline access # "approved" before your first run
260
484
  ```
@@ -377,7 +601,7 @@ Under `hardened_user_separation` each process has an 8 GiB data-memory limit, so
377
601
  base model straight to the GPU (`device_map` or 4-bit loading) rather than building it in
378
602
  CPU memory. While a job runs, about $1.78 of credits is held per window of about an hour;
379
603
  it settles at the actual cost after teardown (the acceptance job cost about $0.06). Full
380
- fine-tunes that return merged weights need larger outputs and are coming soon.
604
+ fine-tunes that return merged weights need larger outputs and are not yet offered.
381
605
 
382
606
  The rest of this section describes how source runs behave.
383
607
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gridlinegpu"
7
- version = "0.1.9"
7
+ version = "0.2.0"
8
8
  description = "Python client for Gridline YAML workload routing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,14 +1,21 @@
1
1
  """Gridline YAML workload client."""
2
2
 
3
+ from ._batch import BatchResult, BatchRun, batch
4
+ from ._batch import slot_names as batch_slots
5
+ from .app import (App, File, Function, FunctionCall, FunctionError, Image, MapRun, Model, Output, model_path,
6
+ output_dir)
3
7
  from .client import GridlineClient, PlannedWorkload
4
8
  from .errors import (
5
9
  GridlineError, LaunchOutcomeUnknown, OutputsIncomplete, ReceiptPersistenceError, SubmissionOutcomeUnknown,
6
10
  WaitTimeout, WorkloadValidationError,
7
11
  )
12
+ from . import finetune # noqa: E402 - after app's names, which its job module imports
8
13
 
9
14
  __all__ = [
10
- "GridlineClient", "GridlineError", "LaunchOutcomeUnknown", "OutputsIncomplete", "PlannedWorkload",
11
- "ReceiptPersistenceError", "SubmissionOutcomeUnknown", "WaitTimeout", "WorkloadValidationError",
15
+ "App", "BatchResult", "BatchRun", "File", "Function", "FunctionCall", "FunctionError", "GridlineClient",
16
+ "GridlineError", "Image", "LaunchOutcomeUnknown", "MapRun", "Model", "Output", "OutputsIncomplete", "PlannedWorkload",
17
+ "ReceiptPersistenceError", "SubmissionOutcomeUnknown", "WaitTimeout", "WorkloadValidationError", "batch",
18
+ "batch_slots", "finetune", "model_path", "output_dir",
12
19
  ]
13
20
 
14
21