internal/sys/sidecar/, internal/api/training/, admin_ui/src/training.ts, internal/db/relational/, sidecar/ai_train/
gligabao can fine-tune Gemma-4 (4B, 12B and 31B, with LoRA adapters) on a cloud GPU and serve the result. The training code is Python and runs on Modal, behind the Python sidecar: the Go server only ever talks to the sidecar, and the sidecar proxies every request to Modal. The Go server starts jobs through the sidecar, follows their progress (status and loss) and keeps a record of every job it started. The admin UI has a training view to run jobs, pick their dataset and watch them.
The pieces
How a job starts
- The endpoint validates the request and, for a
dataset, loads that dataset’s file from the Go server’s own storage (an unknown dataset is a400). It calls the Go client. - The client calls
POST /training/job_starton the sidecar, the only address the Go server knows. The request carries the dataset file asdataset_content. The sidecar forwards the request unchanged to the Modal web API, which stores the dataset on its data volume (internally, there is no dataset endpoint) and spawns the job on the compute provider without waiting for it and reports back with aTrainingJob; the sidecar returns that answer as it is. A request the Modal web API rejects (for example a baddataset_content) is a400that reaches the Go server unchanged. If Modal cannot be reached, the sidecar answers502. - The server stores the
TrainingJobas a new row ofgb_training_job_runs, answers with that row, and hands the job to the training monitor.
How progress comes back
running, training, completed with the final loss, failed with the error) and one progress event per trainer log (step, loss, learning rate, epoch). Because the Go server stores them, the loss graph of a finished job is still there after the sidecar’s log is gone, and the Go server resumes following unfinished jobs after a restart. The log keeps every event of a job, so a Go server or sidecar that was offline while the job trained gets the events it missed when it reconnects (from=<last id + 1>). See Training progress.
A typical workflow
- Set up the sidecar with your Modal tokens once, and deploy the Modal app: with the Deploy button of the admin UI’s training view (Modal app lifecycle) or
modal deploy app.py(Running on Modal). - Add one or more datasets: a subdirectory of the training datasets directory (
build/datasets_training/) with adataset.tomlmanifest and the JSONL file (or upload a file throughPOST /v1/gb/training/datasetsor the admin UI’s training view). The server finds them by their manifests, and sends one to the sidecar with the job that uses it. A job needs a dataset; there is no default one. Format and manifest: Training datasets. - Pick a size (Model sizes) and a dataset; start with
4band 100 to 200 steps to check that the loss falls. - Start the job: the
Runbutton of the admin UI, the endpoint it calls, ormodal run app.py --action traindirectly (that one bypasses the sidecar and is not recorded by the server). - Watch it in the training view of the admin UI (feed and loss graph), or through
GET /v1/gb/training/events; the sidecar’s own views areGET /training/job_eventsandGET /training/job_progress(sidecar API). - Run inference on the trained model: pick it in the admin UI’s Trained models table, or
POST /v1/gb/training/models/{id}/infer(Trained models). Every completed job is a new model, so you can train again with other data, steps or size and keep all of them.
Configuration
The address is used as given. On the docker compose network that is
http://sidecar:8787 (see With docker compose). For a job to start, the sidecar at that URL must itself reach the Modal web API: it needs MODAL_TOKEN_ID and MODAL_TOKEN_SECRET, and the Modal app deployed (from the admin UI, see Modal app lifecycle, or with modal deploy app.py; or SIDECAR_MODAL_WEB_URL set, for modal serve); see the sidecar’s configuration. The Go server pings the sidecar’s /health once on startup; if that fails it runs without a sidecar and the training endpoints answer 503. The sidecar has no authentication, so keep it on a private network.
The settings of the sidecar itself (SIDECAR_BACKEND, SIDECAR_APP_NAME, the Modal tokens) are in Python sidecar; the settings of the Modal app (SIDECAR_PROVIDER, SIDECAR_GPU, the volumes, …) are in Running on Modal.
In this section
- Training jobs: the
TrainingJobstruct, the training endpoints, thegb_training_job_runstable and the Go client. - Training datasets: the dataset format, the training datasets directory and its manifests, listing and uploading on the Go server, how a dataset reaches the sidecar,
TrainingDataset. - Model sizes: 4B, 12B and 31B, backends, GPUs, cost, LoRA settings, troubleshooting.
- Trained models: where a job’s weights are saved, the
gb_trained_modelstable, how inference picks a model, and how to add an inference endpoint. - Model deploying: the deployer hook that receives a trained model’s weights as local files, how they are fetched and verified, and the
gb_trained_model_deploymentsrecord. - Evals of trained models: the Eval button: a model is asked its training dataset’s questions, one per prompt, and an LLM judges the answers in batches.
- Sidecar training API: the REST endpoints the sidecar serves and the Modal web API behind them.
- Running on Modal: the code layout, the
modalcommands, the sidecar’s configuration, tests. - Modal app lifecycle: deploy, watch (live log), cancel and stop the Modal app and list its volumes from the sidecar and the admin UI, without the CLI.
- Providers and extending: how the provider abstraction works, adding a provider, a model size or a function.
Not tracked yet
- A job’s end is only known through the events the training container writes. If the container dies without writing
failed(killed, out of memory, an image that fails to build), nothing reports it: the Go server marks the jobfailed3 hours after it started. - Jobs started directly with
modal rundo not go through the sidecar and are not in the database; only jobs started through the training endpoint (or the UI) are. - There is no endpoint to cancel a job.