Metadata-Version: 2.4
Name: gridlinegpu
Version: 0.1.9
Summary: Python client for Gridline YAML workload routing
Author: Gridline GPU
License: Proprietary
Project-URL: Documentation, https://gridlinegpu.com/cli
Project-URL: Repository, https://github.com/GridlineGPU/gridline-gpu
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: PyYAML<7,>=6.0.2
Provides-Extra: source-contract
Requires-Dist: rfc8785==0.1.4; extra == "source-contract"
Requires-Dist: unicodedata2==16.0.0; extra == "source-contract"

# Gridline Python SDK

Run your own Python code on one GPU as a finite job (**source runs**: batch inference, fine-tuning,
evaluation) and get your declared output files back, verified. The package also submits legacy YAML
routing workloads, and installs the `gridline` command in the same Python environment on macOS and
Linux.

## Self-serve quickstart (source runs)

1. **Sign up** at https://gridlinegpu.com/signup and verify your email.
2. **Request access** at https://gridlinegpu.com/get-started: accept the partner beta terms and
   describe your use case in a few lines.
3. **Wait for approval.** Gridline approves each request by hand and grants your account beta
   credit; the get-started page shows the status. There is no payment step: jobs draw on that
   credit.
4. **Install** the SDK with the source extra (Python 3.10 or newer):

   ```sh
   python -m pip install 'gridlinegpu[source-contract]>=0.1.9,<0.2'
   ```

5. **Create a token** once. Browser approval saves a private, scoped token file; it is never
   printed. Keep it out of source control and AI chats. Then check your access and credit:

   ```sh
   gridline setup --apply
   gridline access          # prints your status and the next step; "approved" means you can run
   python -c "from gridlinegpu import GridlineClient; print(GridlineClient().credits()['availableUsdMicros'])"
   ```

6. **Run a first job** on the curated `gridline/hf-torch-2.5@r1` runtime (CPython 3.11, CUDA 12.4,
   torch 2.5.1, transformers 4.46.3). Put two files in an empty folder. `gridline.yaml`:

   ```yaml
   version: 2
   name: first-job
   task:
     kind: application
     class: inference
     source:
       include: [job.py]
     command: [python, job.py]
   runtime: gridline/hf-torch-2.5@r1
   outputs:
     results:
       path: outputs/results.json
       max_bytes: 1048576
   resources:
     gpu:
       count: 1
       minimum_vram_gib: 8
   ```

   and `job.py`:

   ```python
   import json, os, torch

   os.makedirs("outputs", exist_ok=True)
   x = torch.randn(1024, 1024, device="cuda")
   with open("outputs/results.json", "w") as f:
       json.dump({"gpu": torch.cuda.get_device_name(0), "sum": float((x @ x).sum())}, f)
   ```

   Then submit it, wait, and download the verified output:

   ```python
   from gridlinegpu import GridlineClient, SubmissionOutcomeUnknown

   client = GridlineClient()
   try:
       job = client.run("gridline.yaml")
   except SubmissionOutcomeUnknown as error:
       job = client.resume(error.submission_id)  # never run() again for the same submission
   result = job.wait()                            # a local timeout never cancels the job
   print(result.outcome, result.failure_code)
   if result.succeeded:
       job.outputs("results")                     # length and SHA-256 checked
   print(job.report()["settlement"])              # what the job held, charged and released
   ```

   Gridline's curated-beta example (`examples/workloads/curated-beta/`) is the same job with a
   small text generation step.

7. **Fine-tune.** The fine-tune example (`examples/workloads/finetune-lora-smollm2/`,
   `gridline-models.yaml`) trains LoRA and QLoRA adapters for SmolLM2-135M on the
   `gridline/hf-finetune-2.5@r1` runtime. `models:` makes the platform fetch the pinned base model
   into `models/base` before your code starts; your code never downloads it:

   ```yaml
   version: 2
   name: finetune-lora
   task:
     kind: application
     class: training
     source:
       include: [train.py, data/train.jsonl, data/eval.jsonl]
     command: [python, train.py]
   runtime:
     curated: gridline/hf-finetune-2.5@r1
   models:
     base:
       source: hf://HuggingFaceTB/SmolLM2-135M
       revision: 93efa2f097d58c2a74874c7e644dbc9b0cee75a2
       files: [config.json, generation_config.json, model.safetensors, special_tokens_map.json,
               tokenizer.json, tokenizer_config.json]
   outputs:
     metrics:
       path: outputs/metrics.json
       max_bytes: 262144
     lora_adapter:
       path: outputs/lora/adapter_model.safetensors
       max_bytes: 33554432
     lora_config:
       path: outputs/lora/adapter_config.json
       max_bytes: 65536
   resources:
     gpu:
       count: 1
       minimum_vram_gib: 16
   ```

   `train.py` loads `models/base` with `local_files_only=True`, trains, and writes each declared
   file under `outputs/`. Submit it like the first job. **Mark every output you need as required**
   (the default). For now an optional output written after a required one may be left out of the
   results, even when the job succeeds.

Without approved access, `run()` is refused with `partner_not_admitted` before anything is
submitted, and the error points back to the get-started page. Source runs need the
`source-contract` extra: it stays optional because one of its packages (`unicodedata2`) has no
prebuilt wheel for some platforms (for example CPython 3.12+ on Linux aarch64), where it needs a C
compiler. Without it, the YAML commands and `client.access()` still work and source calls raise
`source_dependencies_missing` with the install line. The "Source runs (beta)" section below has the
limits, costs and failure codes.

## YAML routing workloads

Submit one YAML workload through Gridline's authenticated API. The Python client stages
files referenced by YAML, returns a route for review, then launches only that exact
fresh plan when you supply a cost ceiling and idempotency key.

```sh
python -m pip install 'gridlinegpu>=0.1.9,<0.2'
```

Run browser setup once. It creates a 30-day scoped token in a private file, attempts to
revoke the temporary browser session, and never prints the token. Keep the token file out of
source control and AI chats.

```sh
gridline setup --apply
```

Use a YAML file beside its referenced inputs. Task, model, engine, providers, output,
budget, and deadline belong in YAML rather than Python arguments.
Server-advertised artifact limits govern each submission. Newer Gridline deployments
use direct, signed resumable Storage uploads for large local inputs; older deployments
may still cap inputs at 40 MiB and outputs at 8 MiB.

```python
from gridlinegpu import GridlineClient

client = GridlineClient()
search = client.search("workload.yaml")
print(search["coverage"], search["offers"])  # Catalog facts may be unknown; coverage may be partial.
plan = client.plan("workload.yaml")
print(plan.review)  # Check selected GPU, exclusions, estimate, and maximum.

# Launch only after reviewing the fresh plan and accepting its maximum spend.
launch = client.launch(plan, max_cost_usd="5.00", idempotency_key="my-job-2026-09-27")
job_id = launch["jobId"]
ready = client.wait(job_id, timeout=7200)
print(ready["job"]["outputs"], ready["job"]["outcome"])
print(client.outputs(job_id, "results"))
```

The same installed package supports a terminal flow:

```sh
gridline workload search workload.yaml
gridline workload validate --file workload.yaml
gridline workload plan --file workload.yaml
gridline workload run --file workload.yaml --apply --max-cost-usd 5 \
  --idempotency-key my-job-2026-09-27
gridline workload status JOB_ID
gridline workload outputs JOB_ID --directory results
```

The paid `run` command plans a fresh quote, prints it, and asks for explicit confirmation.
For an approved noninteractive run, add `--yes` and explicit `--profile production`
alongside `--apply`, a cost ceiling, and an idempotency key. Cancellation similarly
requires `--apply` and confirmation. A custom
token path may be passed as global `--token-file` or through `GRIDLINE_API_TOKEN_FILE`.
Workload commands also honor `GRIDLINE_API_TOKEN` when no token file is selected.
The existing standalone Bun CLI remains available during transition; check which
`gridline` executable your shell selects when both are installed. Run
`python -m gridlinegpu` to choose this package's command explicitly.
Use `gridline auth token list` to find a token ID and
`gridline auth token revoke TOKEN_ID --apply` to revoke it. Both require fresh browser
approval; the Python CLI never stores a privileged refresh token.
After revoking a token, remove its local file before running setup again; setup never
overwrites an existing file.

`plan()` validates YAML and checks advertised task readiness before reserving or
uploading inputs. An explicitly unavailable managed ASR/LLM runtime, minimal managed
model resolver, or custom finite path raises `GridlineError` with code
`planning_unavailable`. Qualify the custom container and enable its launch path, or
use available managed runtime/full YAML settings as indicated by the error. Missing
task readiness fields on older servers preserve existing behavior. Custom endpoint
support remains a server planning decision; finite readiness does not describe it.
`plan()` stages input bytes but does not allocate a GPU. `launch()` never silently
changes a plan or retries an ambiguous paid request. If launch outcome is unknown,
inspect `LaunchOutcomeUnknown.workload_id` with the same idempotency key; do not use a
new key. `outputs()` verifies byte count and SHA-256 before creating private files.
Cancellation requests stop; cleanup and final provider billing can remain pending.
For finite jobs, `wait()` returns when outputs are complete, even if cleanup and billing
leave `outcome` pending. Check `status()` later for final settlement.

`search()` reads only YAML and connected-provider catalogs. It does not inspect or
upload referenced inputs, resolve a model or image, create a workload, or allocate a
GPU. `availability()["searchAvailable"]` is separate from planning and execution
readiness. Missing price, VRAM, capacity, or topology facts remain unknown. Partial
catalog coverage and unsupported execution remain visible; a search result is not a
launch quote. `scopeStatus: "empty_scope"` with `catalogCompleteness: "not_applicable"`
means no accounts were searched. Servers with catalog search disabled return a
search availability error. The CLI prints the same JSON response and also accepts
`gridline workload search --file workload.yaml`.

When worker progress is enabled on the hosted API/control service and supported by the
deployed worker image, `status()` also returns optional `progress`:

```python
status = client.status(job_id)
progress = status.get("progress")
if progress:
    for event in progress["events"]:
        print(event["stage"], event["observedAt"])
```

Events are bounded, ordered worker reports for `progress["attemptId"]` and its fence.
Stages include input staging, model preparation/loading, inference start/completion,
and output upload. `observedAt` is the server's receipt time. Poll no faster than every
five seconds; deduplicate by attempt/fence/stage. Missing progress or skipped stages
mean unavailable evidence, not failure or an inferred stage. Inference completion
does not imply stored outputs, job success, cleanup, or settled billing. `wait()` keeps
its existing completion behavior and does not stream progress callbacks.

## Source runs (beta)

Source runs send your own Python code and inputs to one GPU, run it as a finite job on an
operator-qualified **curated runtime**, and return your declared output files. They are in a
beta funded by credits Gridline grants to each approved account: there is no top-up or payment
step in the SDK. Request access first (see the quickstart above); `client.access()` and
`gridline access` show where your request stands.

```sh
python -m pip install 'gridlinegpu[source-contract]>=0.1.9,<0.2'
gridline setup --apply   # once: saves a private, scoped token file
gridline access          # "approved" before your first run
```

Since 0.1.9 the SDK has no switch of its own: `GRIDLINE_SOURCE_SDK_ENABLED` is ignored, and the
server decides access (`partner_not_admitted` or `partner_suspended` refuse a run before anything
is submitted).

**The workload.** Put a `gridline.yaml` beside your code. Name a curated runtime instead of
declaring dependencies: `gridline/hf-torch-2.5@r1` provides CPython 3.11 and CUDA 12.4 with
`torch==2.5.1`, `transformers==4.46.3`, `accelerate==1.1.1`, `safetensors==0.4.5`,
`tokenizers==0.20.3`, `datasets==3.1.0`, `pandas==2.2.3`, `pyarrow==18.0.0` and
`numpy==1.26.4`. Do not add `python`, `requirements` or a lockfile next to a curated runtime
(refused as `source_runtime_curated_conflict`). A curated runtime named before this
SDK release or the server offers it is refused before anything is uploaded
(`curated_runtimes_unavailable`).

```yaml
version: 2
name: batch-generate
task:
  kind: application
  class: inference
  source:
    include: [job.py]
  command: [python, job.py]
runtime: gridline/hf-torch-2.5@r1
outputs:
  results:
    path: outputs/results.json
    max_bytes: 1048576
resources:
  gpu:
    count: 1
    minimum_vram_gib: 8
```

To set environment variables, use the mapping form:
`runtime: {curated: gridline/hf-torch-2.5@r1, environment: {MY_FLAG: "1"}}`. Temp, cache
and home directories are the platform's and cannot be redirected (`TMPDIR`,
`XDG_CACHE_HOME`, `HOME` and `HF_HOME` are refused).

**Limits during the beta.**
- Inputs: each local input file is at most 512 MiB.
- Outputs: declare each one under `outputs`. Each file is at most 128 MiB (`max_bytes`;
  8 MiB when omitted), at most 1 GiB in total, and at most 32 outputs per job.
- Outputs are required unless marked `required: false`. During the beta, an optional
  output can be left out of the results even when your code wrote it; the job still
  succeeds. Keep every output you need required.
- No network at run time (details below): the code, inputs and models it needs must
  arrive with the job.
- Leave out `budget` and `execution.deadline_seconds`: beta routing places jobs best-effort
  and refuses either constraint (`budget_feasibility_unknown` /
  `deadline_feasibility_unknown`).
- One GPU per job (`resources.gpu.count: 1`).
- Credit holds: while a job runs, its funded hour window holds about $1.78 of your credit (one
  hour of compute plus a 5-minute stop at the $1.50 per hour price ceiling, disk and settlement).
  A few minutes after the machine is removed, the job is charged what it actually used and the
  rest of the hold is released. `client.credits()` shows granted, held, charged and available
  credit; a run with too little available credit is refused with `credit_insufficient`.

**Run it.** `run()` validates, captures your selected files, uploads them and returns a
`JobHandle` once the job is admitted. Everything after that happens server-side.

```python
from gridlinegpu import GridlineClient, SubmissionOutcomeUnknown

client = GridlineClient()
try:
    job = client.run("gridline.yaml")
except SubmissionOutcomeUnknown as error:
    job = client.resume(error.submission_id)  # never run() again for the same submission
print(job.job_id)

result = job.wait()            # polls status; a local timeout never cancels the job
print(result.outcome, result.failure_code)
if result.succeeded:
    print(job.outputs("results"))  # verified files, written under results/
print(job.report()["settlement"])  # reserved, held, charged and released credits for this job
```

`job.status()` reads the current state without waiting, and `job.cancel()` requests a stop.
`job.isolation_tier()` names the isolation tier the job's code ran under (`kernel_cgroup` or
`hardened_user_separation`, see the security page), or `None` before a machine is allocated.
Under `hardened_user_separation` each process of your code has an 8 GiB data-memory limit
(and 4 GiB per written file): an allocation past it fails inside your code (`MemoryError` in
Python), so the job usually ends `workload_runtime_failed`, not `workload_out_of_memory`.
`client.history(limit=20)` lists your recent jobs, newest first. `report()` and `history()`
raise `report_unavailable` / `history_unavailable` until those reads are switched on for
the beta; the job itself is unaffected. A complete minimal
job is in `examples/workloads/curated-beta/`.

**Fine-tuning (beta): LoRA/QLoRA on `gridline/hf-finetune-2.5@r1`.** The
runtime provides CPython 3.11 and CUDA 12.4 with `torch==2.5.1`, `transformers==4.46.3`,
`peft==0.13.2`, `trl==0.12.1`, `bitsandbytes==0.44.1`, `accelerate==1.1.1` and
`datasets==3.1.0`. Your code has no network, so name the base model under `models:` with a
pinned Hugging Face commit revision; the platform fetches and verifies it into
`models/base` before your code starts:

```yaml
runtime: gridline/hf-finetune-2.5@r1
models:
  base:
    source: hf://HuggingFaceTB/SmolLM2-135M
    revision: 93efa2f097d58c2a74874c7e644dbc9b0cee75a2
    files: [config.json, model.safetensors, tokenizer.json, tokenizer_config.json]
outputs:
  lora_adapter:
    path: outputs/lora/adapter_model.safetensors
    max_bytes: 33554432
  lora_config:
    path: outputs/lora/adapter_config.json
    max_bytes: 65536
```

A complete job (LoRA in bf16 and QLoRA in 4-bit on SmolLM2-135M) is
`examples/workloads/finetune-lora-smollm2/gridline-models.yaml`. Keep every adapter output
required (see the limits above): each file at most 128 MiB, at most 1 GiB per job, one GPU.
Under `hardened_user_separation` each process has an 8 GiB data-memory limit, so load the
base model straight to the GPU (`device_map` or 4-bit loading) rather than building it in
CPU memory. While a job runs, about $1.78 of credits is held per window of about an hour;
it settles at the actual cost after teardown (the acceptance job cost about $0.06). Full
fine-tunes that return merged weights need larger outputs and are coming soon.

The rest of this section describes how source runs behave.

**Network.** Source runs have no network access, only Unix domain sockets. No TCP or UDP,
localhost included. So these fail: local HTTP servers (FastAPI, TGI), Ray, and any pip or
Hugging Face download at run time. vLLM (offline `LLM()` included) and NCCL or
torch.distributed (whatever the init method) are expected to fail. The error is typically
`PermissionError: [Errno 1] Operation not permitted`, or a library's own connection or
offline error. Declare models and inputs: they are fetched by the platform, not by your
code, and staged before your code starts. io_uring is not available either. If the
machine cannot enforce this isolation, your code does not start and the job ends with
`isolation_unsupported`: "The allocated machine cannot enforce the workload's required
isolation." The job's credit hold is released and you are not charged for it. Submit a
new run; contact support if it repeats.

**Pinned models.** Gridline fetches each declared model at its pinned commit. If a pinned
model file has changed or is missing when it is fetched for your run, the run stops and
that attempt is used up; it is not retried. A machine was already started, so its time
is charged to your credits. During the beta, a used-up attempt costs at most about $0.50:
up to roughly 20 minutes (15 minutes of startup plus one 5-minute stop cycle) at the $1.50
per hour price ceiling, disk included.
Check that pinned models are still public, then run again.

**No GPU capacity.** If the selected GPU is not available, no machine is started and that
attempt is not charged. The status reads "The selected GPU was not available, so no
machine was started and this attempt was not charged. Trying the next matching offer." If
no matching GPU is left and no machine was started for the job, it ends with
`routing_attempts_exhausted`: "No matching GPU was available right now. No machine was
started for this job, so these attempts were not charged. Try again later or widen the GPU
choice." A machine that did start is charged to your credits for the time it ran, whatever
the outcome.
