# How to train small machine learning models on CPU sandboxes Build an image with scikit-learn or CPU-only PyTorch, train in a sandbox with every vCPU busy, and copy the model file out at the end. **On Runtime a 45-minute training run on 4 vCPU and 8 GiB costs 12 cents with all four cores busy**, at the rates in force on 25 September 2026, and the sandbox stops billing the moment the run ends. Runtime runs on CPUs only, with no GPU sandboxes, so it fits classical models such as gradient-boosted trees and linear models, and small neural networks, rather than large deep learning models. ## The short answer Build the image once, upload the data, train, and download the model: ```ts check import { readFile, writeFile } from "node:fs/promises"; import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "train", recipe: { pip: ["scikit-learn", "joblib"] } }); // once await using sbx = await runtime.sandboxes.create({ image: "train", vcpu: 4, memoryMiB: 8192, network: { internet: false }, // training needs nothing from outside }); await sbx.files.write("/workspace/data/train.csv", await readFile("train.csv")); await sbx.files.write( "/workspace/train.py", [ "import joblib, pandas as pd", "from sklearn.ensemble import HistGradientBoostingClassifier", "from sklearn.model_selection import train_test_split", "df = pd.read_csv('data/train.csv')", "X, y = df.drop(columns=['label']), df['label']", "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0)", "model = HistGradientBoostingClassifier(max_iter=300).fit(X_tr, y_tr)", "print('accuracy', model.score(X_te, y_te))", "joblib.dump(model, 'model.joblib')", ].join("\n"), ); const run = await sbx.exec("python3 train.py", { check: true, timeoutMs: 3_600_000 }); console.log(run.stdout); await writeFile("model.joblib", await sbx.files.read("/workspace/model.joblib")); ``` ```python check from withruntime import Runtime TRAIN = """import joblib, pandas as pd from sklearn.ensemble import HistGradientBoostingClassifier from sklearn.model_selection import train_test_split df = pd.read_csv('data/train.csv') X, y = df.drop(columns=['label']), df['label'] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=0) model = HistGradientBoostingClassifier(max_iter=300).fit(X_tr, y_tr) print('accuracy', model.score(X_te, y_te)) joblib.dump(model, 'model.joblib') """ runtime = Runtime() runtime.images.build(name="train", recipe={"pip": ["scikit-learn", "joblib"]}) # once with runtime.sandboxes.create(image="train", vcpu=4, memory_mib=8192, network={"internet": False}) as sbx: with open("train.csv", "rb") as file: sbx.files.write("/workspace/data/train.csv", file.read()) sbx.files.write("/workspace/train.py", TRAIN) print(sbx.exec("python3 train.py", check=True, timeout_ms=3_600_000).stdout) with open("model.joblib", "wb") as file: file.write(sbx.files.read("/workspace/model.joblib")) ``` pandas and NumPy are already in the default image, so the recipe adds only what training needs. An `exec` may run up to 24 hours; for a run longer than the sandbox's one-hour lease, call `sbx.keepAlive()` in TypeScript or `sbx.keep_alive()` in Python first. ## Small neural networks with PyTorch on CPU PyTorch publishes CPU-only builds on its own package index, the right choice for a machine with no GPU. A recipe's `commands` run that install while the image builds: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "torch-cpu", recipe: { commands: ["pip install torch --index-url https://download.pytorch.org/whl/cpu"], pip: ["numpy", "pandas"], }, }); await using sbx = await runtime.sandboxes.create({ image: "torch-cpu", vcpu: 4, memoryMiB: 8192 }); const check = await sbx.exec( "python3 -c 'import torch; print(torch.__version__, torch.get_num_threads())'", ); console.log(check.stdout); ``` The check prints how many threads PyTorch will use; compare it with the sandbox's `vcpu` so training keeps every core busy. Size the model to the machine: a multilayer perceptron on tabular data or a small convolutional network is a CPU job, and a model that needs a GPU to train in reasonable time is not one for this page. ## Search hyperparameters in parallel Each trial is its own sandbox, so a search runs as wide as the account allows, up to 200 vCPUs at once on a paid account to start. Prepare one sandbox with the data on disk, then fork it: every copy starts with the files, installed packages and memory of the original, so no copy downloads the data again. ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await using base = await runtime.sandboxes.create({ image: "train", vcpu: 4, memoryMiB: 8192 }); // train.py, which takes --learning-rate, and its data/ folder: await base.files.upload("./trainer", "/workspace/trainer"); const rates = ["0.03", "0.1", "0.3"]; const copies = await base.fork({ count: rates.length }); const scores = await Promise.all( copies.map(async (copy, i) => { const run = await copy.exec(["python3", "train.py", "--learning-rate", rates[i]!], { cwd: "/workspace/trainer", timeoutMs: 3_600_000, }); await copy.stop(); return [rates[i], run.stdout.trim()]; }), ); console.log(scores); ``` Copies get the source's size and are billed as a create of that size would be; the snapshot a fork takes for itself is deleted and never charged ([snapshots and forks](/docs/javascript#snapshots-and-forks)). ## Keep checkpoints and datasets | What | Where to put it | Cost | | --------------------------- | ---------------------------------------------------------- | ------------------------------------- | | A dataset used by many runs | A read-only volume copy, or your bucket mounted as `/data` | Volume: 153 microdollars per GiB-hour | | Checkpoints during a run | A read-write volume; run `sync` after each save | Charged on the volume's full size | | The finished model | `files.read` to your own storage | Nothing on Runtime once stopped | | The whole trained machine | A snapshot, kept 1 to 365 days | $0.08 per GB per 30-day month | ## What CPU training needs | Need | How Runtime covers it | | -------------------------- | ----------------------------------------------------------------------- | | ML libraries | pip packages in a recipe image; NumPy and pandas in the default image | | All cores working | Shared CPU bursts up to `vcpu`; `cpu: "reserved"` guarantees every vCPU | | Training data kept private | `network: { internet: false }`, enforced on the host | | Parallel trials | Forks with memory and files; 100 sandboxes at once on a paid account | | Runs longer than an hour | `exec` up to 24 hours with `keepAlive` | | Watching the run | `sbx.metrics()` for measured CPU and memory | ## What it costs One run of 45 minutes on 4 vCPU and 8 GiB, with all four cores busy throughout: ``` CPU: 0.75 h × 4 vCPU × $0.025 = $0.075 Memory: 0.75 h × 8 GiB × $0.0075 = $0.045 Total: $0.12 ``` Fifty such runs a month cost $6.00. A run that waits on data loading uses less measured CPU and costs less, down to a floor of a twentieth of a vCPU. Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory ([pricing](/docs/pricing)). ## Start ```bash no-run npx withruntime sandbox run --trial -- python3 -c 'import numpy, pandas; print(numpy.__version__, pandas.__version__)' ``` Approve the connection in your browser the first time. Trial sandboxes go up to 2 vCPU and 4 GiB; add credit for 4 vCPU and more. Related: [scientific computing](/use-cases/scientific-computing), [data analysis agent](/use-cases/data-analysis-agent), [RL environments](/use-cases/rl-environments), [Python in a sandbox](/languages/python). ## Sources - [PyTorch: previous versions](https://pytorch.org/get-started/previous-versions/): CPU-only wheels installed with `--index-url https://download.pytorch.org/whl/cpu`, read 25 September 2026. Facts on this page were checked on 25 September 2026.