Skip to main content

remote-compute-modal

Run GPU jobs on the user's own Modal account via host.compute.create('byoc:modal', ...). Covers the create→submit→wait_for_notification flow, the compute_provider kernel for env setup, image/volume resolution, and the two approval cards. Load once you've decided to dispatch to Modal.

Ir para a instalação

Informações da origem

Repositório
HughYau/AcademicForge
Última atividade na origem
2 de julho de 2026 às 10:54
Idioma detectado do SKILL.md
inglês
Estrelas
2.549
Forks
152

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
15 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
remote-compute-modal
description
Run GPU jobs on the user's own Modal account via host.compute.create('byoc:modal', ...). Covers the create→submit→wait_for_notification flow, the compute_provider kernel for env setup, image/volume resolution, and the two approval cards. Load once you've decided to dispatch to Modal.
license
Apache-2.0
You're dispatching to the user's Modal account: containers spin up in Modal's cloud, on hardware you name in plain terms (gpu/cpu/memory/timeout), under their workspace, on their bill. That's the reason there are two approval cards rather than one, and the reason the env-setup surface is a separate kernel rather than something you can call inline from the control-plane kernel: each card is the user consenting to a specific, bounded use of their credential, and the architecture keeps those grants legible by keeping the surfaces apart. If `compute.create('byoc:modal', …)` returns `unknown provider 'byoc:modal'`, Modal isn't enabled in this install — ask the user to enable it under *Settings → Compute → Modal* (or surface the prompt; you can't enable it for them). For first-time environment setup, see `env-setup.md` in this skill directory — it walks through driving the `compute_provider` kernel to build and record images. ## Two timeouts, one timeline Modal has two deadline timers. Both live INSIDE the sandbox, and neither can cost you the outputs of a job that ran. The timeline every job runs on: 1. Container life starts at sandbox creation — staging the inputs counts against it, so a big upload spends container time before the job runs. 2. The job runs. If it exceeds its own budget (the job timeout below), it is TERMed there. 3. Near end of container life — one harvest margin before the provider destroys the sandbox — the in-sandbox **harvest watchdog** TERMs whatever is still running. It runs on the sandbox's own clock, so a wrong desktop clock or a slow upload can't make it fire late. 4. After any TERM there is a grace window for checkpoint-on-TERM handlers to flush, then the process group is stopped. 5. Whatever is under `./out/` (plus the logs) is tarred and staged UNCONDITIONALLY — on success, timeout, failure, and crash alike — and harvested back into the workspace. Staging is unconditional; delivery isn't. If the stream is refused or gives up (`result_rejected`, `harvest_failed`), the staged copy waits on the sandbox, and a third, post-job timer — the **idle watchdog** — terminates it after ~30 min of inactivity. See "When the job fails". The **container timeout** (`provider_params.modal.timeout`) is how long the sandbox lives; omitted, it fills from the Settings default for this provider (*Settings → Compute → Modal*; ceiling: Modal's 24 h platform lifetime, minus the staging margins — 85,500 s). The **job timeout** (`timeout_seconds` on `submit_job`) is an optional runaway guard for one job; omitted, it defaults to the container's remaining life minus the harvest margin. Name one when you know the job's budget — a hung job then costs that budget, not the whole container, and the warm container survives for the next submit. A deadline-ended job lands as `status: 'timed_out'` — not a generic failure — with its partial outputs already harvested, plus a note saying the deadline (not the workload) ended the run and suggesting the remedy: a larger `timeout_seconds`, or resuming from a harvested checkpoint. A deadline ends the run, never the results. ## Two surfaces, one provider You reach Modal two ways. They share confinement and credentials but answer different questions, and confusing them is the most common way to waste a turn. `host.compute.create('byoc:modal', provider_params={'modal': {...}})` is the **job surface**. It runs in the `repl` tool — same as `remote-compute-ssh` — and is what you use for anything with inputs/outputs you want harvested into the workspace, anything on a GPU, and anything long enough that you'd want to reattach if the daemon restarts. The call itself is a stateless constructor — the **tier card** (provider, image, `1× A100-40GB · 8 CPU · 32 GB`, container timeout, volumes) and the actual Sandbox creation both happen on the first `submit_job()`. `submit_job`/`result`/`call_command`/`attach_job`/`close` then work exactly as for SSH: `.result()` is **non-blocking** — the daemon's poller probes the sandbox, harvests `out.tar.gz` into `hpc/<jobId>/`, and emits a `compute_done` notification when done. To wait, exit the cell after `submit_job` and use the `wait_for_notification` brain-tool. What reaches the remote is what was in the workspace files you named, and what comes back lands under `hpc/<jobId>/` through the same hardened extractor. `compute_provider({'provider': 'modal', 'code': '…'})` is the **environment surface** — a Python shell with the user's Modal SDK already authenticated, running in its own confined process. Use it to *prepare* compute: build container images JIT in the user's workspace, populate model-weight volumes by running the downloads on Modal's own infrastructure (so wide-internet fetches never traverse the local allowlist), check what assets already exist, run a short CPU smoke probe. Its first cell in a session fires the **kernel card** — *"environment setup — build images, populate volumes, CPU probes ≤30 min; GPU jobs ask separately"* — and once granted, subsequent cells in this kernel's lifetime (idle-timeout ~15 min) run without further prompts. The kernel will reject `gpu=` on `Sandbox.create` and clamp `timeout` to ≤30 min — the kernel's own probe budget, separate from the job surface's container timeout. It's a redirect to the job surface, not a fence; you've already been handed the credential. **Terminate every sandbox you create here** — `sb.terminate()` in the same cell, in a `try/finally` if you `exec` in between (`build_env`'s hydrate models this). Nothing reaps it mid-session: the idle timeout kills the *kernel*, never your sandboxes — the ≤30-min clamp only bounds how long a forgotten one bills. The shape of a good run is: read what's already known about this workspace, decide whether the env you need exists, build it via the `compute_provider` kernel if it doesn't, then run the actual job through `compute.create()`. The two cards appear at most once each per session — and on a warm second session with a project-level grant, neither. ## Workflow Every `host.compute.*` call here runs via the **`repl` tool**; the `compute_provider` kernel is reached via the **`compute_provider` tool**. Neither is the `python` tool. All three share your workspace directory but not memory — pass data through files. Start with `compute_details({provider: 'byoc:modal', mode: 'read'})`. Below the orientation line is the per-workspace ledger — `### env:<name>@<spec_sha>` blocks recording images that already exist in this user's Modal workspace, the volumes they pair with, and any free-text notes a previous session left. The `spec_sha` is the content hash of the env's source file in this binary, so a ledger entry whose hash matches is one you can use without rebuilding: pass its image ref straight to `compute.create()` and the kernel card never appears. A mismatch means the env definition changed in this release — the old image still works but isn't what the current env file describes, so treat it as absent. If what you need isn't in the ledger, build it in a `compute_provider` cell. Two helpers are pre-bound there alongside `modal` and `app`: - `list_envs()` → `{name: META, …, '_envs_dir': path}` for every bundled env (packages, GPU tier, secrets it needs) - `build_env(name, *, hydrate=False)` → builds the image, returns `{'image': 'im-…', 'spec_sha', 'volumes', 'env', 'hydrate'}` To inspect what an env actually installs (the `modal.Image` chain) before building, read the source from the **control-plane kernel** (a `repl` cell) — it's a skill asset: ```python # repl tool print(host.skills.read('remote-compute-modal', 'envs/proteomics_jax_gpu.py')['content']) ``` Don't `find` for it from bash — the path differs across builds. ```python # compute_provider tool, provider='modal' need = {'transformers', 'torch'} envs = list_envs() print({n: m for n, m in envs.items() if not n.startswith('_') and need <= set(m.get('packages', []))}) # → {'proteomics_gpu': {'packages': [...], 'gpu_default': 'A100-80GB', ...}} r = build_env('proteomics_gpu', hydrate=True) print(r['image'], r['spec_sha'], r['volumes']) ``` Carry `r['image']` (the `im-…` reference, NOT the env name) back to the control-plane kernel (a `repl` cell) and pass it to `provider_params.modal.image` — and, for a bundled env, its NAME as `provider_params.modal.env`: the host then reads the env's `META["egress_domains"]` from the SHIPPED file at every submit, so the declaration survives rebuilds (a pointer into the shipped catalog, not a power). The two kernels share a working directory but not memory, so the simplest handoff is a workspace JSON file: ```python # compute_provider tool — last line of the build cell import json; json.dump(r, open('built_env.json', 'w')) ``` Append a `### env:<name>@<spec_sha>` block to `compute_details` so the next session reads the `im-…` ref straight from there and skips the `compute_provider` cell entirely. **Pass `r['image']` (the `im-…` ref) to `provider_params.modal.image`.** The adapter will resolve a bare env name from the ledger as a fallback, but that's brittle — the ledger entry can be stale or absent. Treat the build as its own task with its own validation; don't fold it into a job submission. See `env-setup.md` for ad-hoc patches and gated weights. For a quick inline call (no inputs, output is just stdout), use `submit_job` with no `inputs`. The `compute_done` notification payload carries `status` / `exit_code` / `featured_files`, plus `error_kind`, `system_hint`, and `deadline_fired` when set — read those disclosures from the notification itself (a deadline-truncated rc-0 job announces partial outputs there). For `stdout_tail`, re-enter the kernel and read it from `.result()` after the notification arrives (`call_command` is SSH-only — it bails on a `byoc:` handle): ```python # repl tool — cell ① submits and RETURNS import json r = json.load(open('built_env.json')) c = host.compute.create('byoc:modal', provider_params={'modal': { 'image': r['image'], # the im-… ref (bare name fallback brittle) 'env': 'proteomics_gpu', # bundled-env NAME → its shipped egress declaration applies 'gpu': 'A100', # timeout omitted → container gets the Settings default 'volumes': r['volumes'], }}) j = c.submit_job( intent='load esm2_t6 on GPU', command='python -c "from transformers import EsmModel; import torch; ' 'm = EsmModel.from_pretrained(\\"facebook/esm2_t6_8M_UR50D\\").cuda(); ' 'print(torch.cuda.get_device_name())"', timeout_seconds=300, # job guard — this probe finishes in 5 min or it's hung ) print('JOB_ID:', j.job_id) # ← cell ends here; kernel never blocks ``` Then exit the cell and use `wait_for_notification`. When the `compute_done` notification arrives, its payload has `status` / `exit_code` / `featured_files`, plus `error_kind` / `system_hint` / `deadline_fired` when set. For `stdout_tail`, re-enter the kernel: ```python # repl tool — cell ② after the compute_done notification print(c.attach_job('<JOB_ID>').result()['stdout_tail']) # non-blocking — poller already harvested c.close() ``` For a full job with inputs and harvested outputs: ```python # repl tool — cell ① submits and RETURNS c = host.compute.create('byoc:modal', provider_params={'modal': { 'image': r['image'], # 'im-…' from build_env(); bare-name fallback exists but is brittle 'env': 'proteomics_jax_gpu', # bundled-env NAME → its shipped egress declaration applies 'gpu': 'A100-40GB', 'cpu': 8, 'memory': 32768, # MiB — Modal's unit, not GiB 'volumes': {'/cache': 'af2-params'}, # mount path → Modal Volume name 'timeout': 3600, # container lifetime, seconds — optional; omitted → Settings default; ceiling: Modal's 24 h platform limit }}) # → tier card → Sandbox.create() under the user's workspace → handle bound to sandbox_id job = c.submit_job( intent='colabfold on target.fasta — 1× A100, ~40 min', command='colabfold_batch target.fasta out/ --num-recycle 3 --model-type alphafold2_ptm', inputs=[{'src': 'target.fasta', 'dst_filename': 'target.fasta'}], outputs=['out/ranked_0.pdb', {'glob': 'out/*.json', 'visibility': 'featured'}, {'glob': '*.log', 'visibility': 'hidden'}], timeout_seconds=2700, # job guard — optional; default = remaining container life − harvest margin ) print('JOB_ID:', job.job_id) # ← cell ends here ``` Then `wait_for_notification` — the `compute_done` payload includes `featured_files`, so you can `save_artifacts(payload['featured_files'])` directly without re-entering the kernel. If you want the full dict (`stdout_tail` / `stderr_tail` / `job_wall_s`): ```python # repl tool — cell ② after the compute_done notification r = c.attach_job('<JOB_ID>').result() # non-blocking read of compute_usage.result save_artifacts(r['featured_files']) c.close() ``` `r['job_wall_s']` is the seconds `run.sh` ran on the remote — the actual GPU/compute duration. `timeout` is the container's lifetime; `timeout_seconds` guards one job inside it. Both are optional: `timeout` fills from the Settings default, `timeout_seconds` from the container's remaining life minus the harvest margin. Name a `timeout_seconds` when you know the job's budget — a hung job then costs that budget and the warm container survives. At either deadline the job is TERMed, given grace, and its outputs are staged and harvested as on success — the job lands as `status: 'timed_out'` (deadline-terminated, outputs harvested), not a generic failure. `inputs=` stage **flat** into the workdir root. `dst_filename` is a bare filename — `'inputs/gfp.fasta'` is rejected at submit (the file would land at `/work/gfp.fasta`, not where the command expects). `src` can be a path; `dst_filename` can't. Omit it to default to `basename(src)`. Need a dir layout? `mkdir -p` it inside `command=`. **Sizing `inputs=` — and when to use a Volume instead.** Inputs are stdin-streamed through a confined path at ~0.65–0.81 MiB/s effective (measured) — budget staging time from that: 1 GiB ≈ 20–26 minutes of upload before the job starts, spent from container life. The host budgets staging from input size with a planning rate it cannot verify (upload speed isn't controllable), so a slower-than-planned upload never endangers the harvest — the watchdog's absolute-clock backstop still protects the staging window; the job just gets less runtime than nominal. If an upload dies outright, the submit fails LOUDLY with a transport error and no phantom job. Practical sizing: code, configs, and small data are what `inputs=` is for; it is workable up to its **1 GiB per-submit cap** (slow but fine — budget the ~20–26 min of staging near the cap). Anything above 1 GiB is refused at submit time, so beyond it a Volume is the only path — and the better one well before that. Volumes (Files-tab import, or a `compute_provider` cell; mount via `provider_params.modal.volumes`) persist on Modal disks across jobs and cost zero upload per submit. **Checkpoint long jobs as they run.** Write progress to `./out/` (or a Volume) periodically: at either deadline the workload gets TERM, then a grace window for checkpoint-on-TERM handlers, and whatever is under `out/` is staged and harvested unconditionally. A job that checkpoints loses at most one interval to a deadline — never the run. Keep `./out/` checkpoints SMALL (≈ ≤100 MB compressed): the harvest stream back to the host runs in a bounded (~2 min) window, so a multi-GB `./out/` risks `harvest_failed` even though staging succeeded — large checkpoints belong on a Volume, with only small summaries under `./out/`. **Only `./out/` is harvested.** The wrapper tars `out/` + `stdout.log` + `stderr.log` — nothing else. `outputs=` globs are a *post-harvest* filter (featured/hidden), not a what-to-collect directive. If your tool writes to cwd, a Volume mount (e.g. `/atlas`), or `$HOME`, end `command=` with `cp -r <results> out/` (or point the tool's `-o`/`--output` at `out/`). A successful job with an empty `out/` returns only the log paths in `output_files` plus a `system_hint` telling you so.
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub