# 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 effect on 26 October 2026. Billing stops the moment the run ends. Runtime runs on CPUs only, with no GPU sandboxes. It fits classical models such as gradient-boosted trees and linear models, and small neural networks, but not 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 the packages that training needs. An `exec` can run for up to 24 hours. The sandbox has no time limit, so a long run needs no extra setup. ## Small neural networks with PyTorch on CPU PyTorch publishes CPU-only builds on its own package index. Use them on 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 the number of threads PyTorch uses. Compare it with the sandbox's `vcpu` to confirm that training uses every core. Size the model to the machine. A multilayer perceptron on tabular data or a small convolutional network is a CPU workload. This page does not cover a model that needs a GPU to train in a reasonable time. ## Search hyperparameters in parallel Each trial runs in its own sandbox, so a search runs as wide as the account allows: up to 200 vCPUs at the same time on a paid account, initially. Prepare one sandbox with the data on disk, then fork it. Each 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 have the source's size and are billed as if each were created at that size. The snapshot that a fork takes for itself is deleted and is 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 | No Runtime charge after the sandbox stops | | The whole trained machine | A snapshot, kept for as long as the account has credit | $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 the same time on a paid account | | Runs longer than an hour | `exec` up to 24 hours; no time limit | | 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, with 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 sandboxes run -- python3 -c 'import numpy, pandas; print(numpy.__version__, pandas.__version__)' ``` Approve the connection in your browser the first time you run it. Without credit, a sandbox can use up to 2 vCPU and 4 GiB. Add credit to use 4 vCPU or 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 26 October 2026. Facts on this page were checked on 26 October 2026.