GPUtw Docs

Vault Storage

Use persistent storage mounted into GPU containers.

Mount path

GPUtw Vault storage is mounted inside running GPU containers at /vault. Use it for datasets, checkpoints, model weights, and outputs that need to survive container restarts or replacement.

Workspace vs Vault

The container workspace is tied to the selected worker and instance lifecycle. Vault is the persistent area intended for files you need to keep across sessions. If your balance is at NT$0 or below, Vault data is retained for 30 days and then deleted unless you top up.

Which one to write to is a question of what the files are:

  • /vault: datasets, checkpoints and model weights — anything that must outlive an instance. Network storage, shared by every instance on the account.
  • /workspace: installing environments (pip, conda, npm), building, and unpacking archives of many thousands of small files. It is the instance's own disk — faster for that kind of work, and deleted with the instance, so copy anything worth keeping into /vault.
Tip

A useful rule: if losing it would cost you a re-download or a re-train, it belongs in /vault. If you could regenerate it with one command, keep it in /workspace.

Operational guidance

Keep generated checkpoints, downloaded datasets, and project artifacts under /vault. Avoid storing secrets in shared notebooks or public web directories.

Getting files in

From the dashboard Vault page you can browse folders, drag-and-drop uploads (large files upload in resumable chunks — no 100 MB limit), and Download from URL to have the server pull a model straight into your vault — no running instance or shell needed. Paste a direct link or a Hugging Face reference and pick a target folder.

Tip

For ComfyUI, place models under models/<category> (checkpoints, loras, vae, …). They appear in ComfyUI's picker after a refresh. Turn on Beginner mode (top bar) for a guided helper panel on ComfyUI instances.

Uploading files remotely

The dashboard's drop zone covers the browser. From CI, a script, or another machine there are four ways into an instance's storage, and picking one comes down to two questions: does an instance have to be running, and does the file need to outlive it.

  • Direct upload to /vault: one HTTPS request with an upload token. No instance, nothing billed for compute. This is the one for CI, cron jobs, and pushing from a server you already run.
  • Resumable multipart upload: the same destination, for files above roughly 90 MB or over any link that may drop. Up to 2 TB per file.
  • SCP to /vault: needs an instance RUNNING, and the files stay after that instance is deleted.
  • SCP to /workspace: needs an instance RUNNING, and the files are destroyed with it — faster for data a training loop reads over and over.

Direct upload — no instance needed. On Dashboard → API Keys choose the Upload token preset. It grants vault:write and nothing else, so it can write into /vault over HTTPS with nothing running — and a token leaked out of a CI log cannot deploy an instance, cannot start GPU spend, and cannot read or download a single file, because listing and downloading are vault:read and that scope is not in the preset.

POST/api/vault/upload?path=datasets
Example
curl -X POST "https://upload.gputw.ai/api/vault/upload?path=datasets" \
  -H "Authorization: Bearer gputw_live_..." \
  -F "[email protected]"
  • ?path=: the destination folder inside your vault. Nested paths are fine, missing folders are created, and omitting it writes to the vault root. It names a folder, not a file — the filename comes from the uploaded file itself.
  • file: the multipart form field name, and it has to be exactly that.
  • Response: 201 with the created entry — name, size, modifiedAt, type — inside the usual data envelope.
Warning

Write-only is not the same as harmless. vault:write overwrites a file of the same name without asking, can delete files, and grows a vault that is billed by size. An upload that would take you past your quota is rejected with 413 and the bytes already written are removed. Rotate the token like any other credential.

Above roughly 90 MB, or on any connection you would not bet a 40 GB transfer on, switch to the resumable API in the next section. The dashboard's drop zone already crosses over at the same point without being told to.

Resumable multipart upload

The chunked API splits one file into parts, each sent as its own request. No single request is anywhere near a body-size limit, a dropped connection costs you one part instead of the whole transfer, and one file can be up to 2 TB — past that your vault quota is the real gate, and it is checked when the session is created, counting every other session you already have in flight.

Four steps: create a session, PUT every part, complete, then poll while the server assembles.

POST/api/vault/uploadsPUT/api/vault/uploads/{id}/parts/{n}POST/api/vault/uploads/{id}/completeGET/api/vault/uploads/{id}DELETE/api/vault/uploads/{id}
Example
# 1 — create the session. Here 'path' INCLUDES the filename.
curl -X POST "https://upload.gputw.ai/api/vault/uploads" \
  -H "Authorization: Bearer gputw_live_..." \
  -H 'Content-Type: application/json' \
  -d '{"path":"models/model.safetensors","size":21474836480}'
# → 201 {"success":true,"data":{"uploadId":"<uuid>","chunkSize":16777216,
#        "partCount":1280,"status":"pending", ...},"error":null}

# 2 — split by the chunkSize the session returned, then PUT each part.
#     Raw bytes, zero-based index, no multipart wrapper.
split -b 16777216 -d -a 4 model.safetensors part-
curl -X PUT "https://upload.gputw.ai/api/vault/uploads/<uploadId>/parts/0" \
  -H "Authorization: Bearer gputw_live_..." \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @part-0000

# 3 — complete once every part is in. 202; assembly runs in the background.
curl -X POST "https://upload.gputw.ai/api/vault/uploads/<uploadId>/complete" \
  -H "Authorization: Bearer gputw_live_..."

# 4 — poll until status is completed (or failed).
curl -H "Authorization: Bearer gputw_live_..." \
  "https://upload.gputw.ai/api/vault/uploads/<uploadId>"
  • path: relative to your vault root and includes the filename, unlike the single-request upload above. It cannot name a folder that already exists.
  • size: the exact byte length of the file, up to 2 TB. Part sizes are checked against it, so it cannot be an estimate.
  • sha256: optional, 64 hex characters. The server hashes the file as it assembles and fails the upload on a mismatch, keeping the staged parts so you can re-send.
  • chunkSize: optional, from 4 MiB to 64 MiB, 16 MiB by default. A fast link can trade fewer requests for larger ones; a slow link should not, because each part still has to finish inside a single request.
  • Part index: zero-based, 0 through partCount - 1. Every part must be exactly chunkSize bytes except the last, which is the remainder — anything larger is rejected with 413.
Tip

Re-sending a part is safe: it replaces the earlier one. GET /api/vault/uploads/<id> returns receivedParts, so after an interruption you ask what arrived and send only the missing indices. That one route accepts vault:read or vault:write, which is what lets a write-only upload token resume and poll its own session.

Info

status runs pendingassemblingcompleted, or failed. Completing a session that is still missing parts returns 400 naming the missing indices, and a failed session can be completed again once you have re-sent them. DELETE /api/vault/uploads/<id> aborts and frees the staged parts; a session left idle for 48 hours expires on its own.

The dashboard's drop zone runs exactly this flow, which is why a large browser upload survives a flaky connection without you doing anything.

The transfer host — for large files

Vault transfers are also served on a hostname of their own, which connects straight to our storage instead of passing through the CDN that fronts the rest of the site. Two things change, and both matter for big files: a single request is no longer capped at 100 MB, and multi-GB transfers run far faster. Everything else is identical — same API, same token, same paths; only the hostname differs.

Example
# the same upload, sent to the transfer host
curl -X POST "https://upload.gputw.ai/api/vault/uploads" \
  -H "Authorization: Bearer gputw_live_..." \
  -H 'Content-Type: application/json' \
  -d '{"path":"models/model.safetensors","size":21474836480}'

It deliberately carries the transfer routes and nothing else:

  • Served there: /api/vault/upload, /api/vault/uploads/…, /api/vault/download, /api/vault/download-zip and /api/vault/download-token.
  • Everything else: returns 404 — signing in, listing your vault, instances, billing. Those stay on the main hostname, behind the protections that sit in front of it.
Tip

Do not hardcode the hostname in a script. Take the site you already call, prefix the domain with upload., and check GET /health on it with a short timeout: if it answers, send transfers there; if it does not, fall back to the normal hostname, where the same routes work under the 100 MB limit.

The dashboard's Vault page already does this for you — the drop zone chunks large files and sends them over the transfer host when one is available.

Browse and download with the API

The same endpoints the dashboard's file browser uses are open to an API key with vault:read. Paths are always relative to your vault root.

GET/api/vault/list?path=models

Returns files[] with name, size, modifiedAt and type (file or directory). Two things to code defensively for:

  • size may be null: on a directory. It carries a recursive total only in the root listing, and even there it is null when the total is not available. Treat null as unknown, never as zero.
  • Listings are not recursive: call it per folder to walk a tree.
GET/api/vault/download?filename=models/model.safetensors

Streams one file as application/octet-stream with an RFC 6266 Content-Disposition, so non-ASCII filenames arrive intact. Byte ranges are supported, which is what makes a large download resumable:

Example
# resume an interrupted download of a 40 GB model
curl -fSL -C - -o model.safetensors \
  -H "Authorization: Bearer gputw_live_..." \
  "https://upload.gputw.ai/api/vault/download?filename=models/model.safetensors"

# or ask for one explicit range
curl -fsS -r 0-1048575 \
  -H "Authorization: Bearer gputw_live_..." \
  "https://upload.gputw.ai/api/vault/download?filename=models/model.safetensors"
  • 206 + Content-Range: returned for a satisfiable Range. Accept-Ranges: bytes is always advertised.
  • 416: returned for a range that starts past the end of the file — it is never silently answered with the whole file, which would corrupt a resume.
  • A single range only: a multi-range request is answered with the entire file rather than a multipart response.
GET/api/vault/download-zip?folder=datasets

Streams a whole folder as one .zip, built on the fly and stored without compression — vault contents are usually already compressed, so this spends no CPU pretending otherwise. Symlinks are archived as links, not followed. A file deleted while the archive is streaming is skipped rather than failing the download.

Tip

A browser <a href> cannot send an Authorization header, which is why the dashboard first calls POST /api/vault/download-token for a 60-second, download-only token and puts that in the URL. With an API key you do not need it — send the header and call the download routes directly. That endpoint rejects API keys with a 403 on purpose.

Copy files over SCP

SCP and rsync reach an instance through the SSH gateway on port 2222, so unlike an upload token they need an instance RUNNING and your public key on Dashboard → SSH Keys — the gateway checks the offered key against the keys on your account and has no password login at all. The username is pod-<instance-id> and the host is the gateway, not the site you sign in to. Both destinations are worth knowing apart:

  • /vault: network storage, shared by every instance on the account, and it outlives them. Datasets, checkpoints and finished weights belong here: they are still there after the instance is deleted, and the next instance you deploy sees the same files.
  • /workspace: the instance's own disk — faster for data a training loop reads repeatedly, and destroyed together with the instance. Anything worth keeping has to be copied into /vault before you delete it.
Example
# one file into /vault
scp -P 2222 train.zip pod-<instance-id>@ssh.gputw.ai:/vault/datasets/

# a whole directory — -r, the same as any other scp
scp -P 2222 -r ./project pod-<instance-id>@ssh.gputw.ai:/workspace/

Use rsync over the same gateway when a transfer may be interrupted or repeated — it picks up where it stopped and skips what is already there:

Example
rsync -avP -e 'ssh -p 2222' ./dataset/ pod-<instance-id>@ssh.gputw.ai:/vault/datasets/
Warning

The instance bills for the whole transfer, and a large upload over SCP can take far longer than the work that follows it. If all you are doing is putting files in place, an upload token costs nothing in compute — send the data first, and boot the GPU once it is already in /vault.