How ftgs meets §2.1's CLI block: what each command calls, the exit codes, the run directory, signals and resume. The module's one public name is main(argv=None) -> int, which the ftgs console script and python -m openftgs.cli both run (Q-124). The module is a package: its __init__ holds the commands and main, and its __main__ runs main for python -m openftgs.cli (#220). The choices the spec leaves open are Q-124 to Q-144. ftgs bench lives in the private module openftgs.cli._bench, which benchmarks.md describes.
Each command parses its options with argparse and calls the library. Nothing is computed in the CLI that a Python caller couldn't do with the same functions:
| Command | Library calls |
|---|---|
convert |
data.convert_n3dv, convert_opencv_yaml or convert_selfcap, one option per keyword, --test-camera and --window N0:N1 among them (D-80) |
cache |
MultiViewVideo.from_manifest, FrameCache.build, and for gpu or mmap FrameCache.open in that placement, which shows the cache fits on its own, not beside a training run (Q-127, Q-99) |
init roma |
RomaInitConfig, roma_init, io.write_init_points with roma_provenance(cfg) as metadata (Q-107) |
train |
config_from_dict, read_init_points, Trainer or Trainer.from_checkpoint, Trainer.train, io.check_run_checkpoints (Q-163), io.save_run_checkpoint (D-83, Q-152), Trainer.evaluate |
render |
the model, read with io.read_exchange and FreeTimeGaussians.from_exchange from an FTGS-IF file, or with io.load_checkpoint, config_from_dict and the FreeTimeGaussians constructor from a checkpoint directory; io.read_render_request, render.render_request with a checkpoint's Δt_f as the temporal index's bucket width (frame_dt, Q-174), io.write_renders with the SHA-256 of the request's bytes |
eval |
the model, with the SHA-256 of its file or of its checkpoint's manifest.json, evaluate_images with a checkpoint's Δt_f as frame_dt (Q-174, Q-183), metrics_report, eval.write_metrics (Q-136) |
export |
io.load_checkpoint, the model, to_exchange with the configuration's background, io.write_exchange (D-84) |
bench |
the §3.4 benchmark its subcommand names, written as BENCH.json (Q-166): slice (B-3) times motion.temporal_slice on a seeded synthesized model; render (B-7) times render.render_request with the temporal index on and off and sets its threshold T (Q-194, Q-195); init and convert (B-8) time roma_init and a rerun of the scene's recorded conversion on the wall clock; the others as benchmarks.md describes them, every subcommand having its benchmark (#225 to #235 retired Q-130's stub); --model MODEL on train-step and render only (D-88); benchmarks.md |
A MODEL path that doesn't exist, as a mistyped checkpoint directory, is exit 2, reported as missing rather than as an unreadable FTGS-IF file; one that exists but can't be reached, under a directory without search permission say, is exit 2 with the operating system's error (Q-142). A request whose sh_degree is above the model's is exit 2 before anything is rendered, since at_time refuses it (Q-20).
Every command first sets PyTorch's intra-op threads, which the CPU math uses (the reference backend, the CPU k-NN), to the number of physical cores, counted from Linux's CPU topology in /sys/devices/system/cpu (the logical CPUs where it can't be read), unless OMP_NUM_THREADS or MKL_NUM_THREADS is set: then the count PyTorch took from the variable stands (§2.4, Q-176). ftgs --help and a usage error run no command and change nothing. The library leaves the setting alone, so a Python caller sets it with torch.set_num_threads if it wants.
The device is CUDA when PyTorch sees one, else the CPU, and the backend follows it, as in the trainer. ftgs render keeps render()'s default backend, gsplat, so on a machine without CUDA it needs --backend reference, which renders on the CPU (Q-125). To run train or eval on the CPU on a machine with a GPU, for example for the bit-exact resume of the reference backend (§2.2), hide the GPU from PyTorch with CUDA_VISIBLE_DEVICES="".
ftgs render with gsplat and ftgs eval on CUDA spread their work over every visible CUDA device, one shard each, as render_request's and evaluate_images' devices, without changing the results (D-54, Q-175), when two or more are visible. With one GPU, or on the CPU, they pass devices=None and run unsharded, so nothing is copied: no second copy of the model and, for eval, no second set of LPIPS networks (Q-186). There is no option for it: CUDA_VISIBLE_DEVICES picks the devices, as it does for init roma.
Single-file outputs, the init points, the renders, METRICS.json and the exchange model, replace an existing file atomically. Each is written to a temporary sibling named .<name>.tmp-<pid>, as checkpoint saves are (Q-25), and then renamed over the target. A write that fails partway leaves the old file, or no file, and removes the temporary. Directory outputs, a converter's OUT and a fresh run directory, must be absent or empty (Q-126).
| Code | Meaning |
|---|---|
| 0 | success; for ftgs bench, the benchmark ran, whatever its budgets say (Q-166) |
| 1 | an unexpected exception, with its traceback |
| 2 | invalid input or environment: a usage error, a file that fails validation, a missing ffmpeg, ffprobe or COLMAP, a benchmark other than ftgs bench convert without a CUDA device, a configuration this build can't run, a missing [init] extra, LPIPS weights that can't be downloaded, RoMa weights missing from torch.hub's cache for ftgs bench init (Q-192), a recorded source video that can't be found for ftgs bench convert (Q-169), a frame cache that doesn't fit in GPU memory, training that runs out of GPU memory, an output that can't be written |
| 3 | a numerical failure: training diverged, or a render isn't finite (in ftgs render, ftgs eval or ftgs train's final evaluation) |
| 130, 143 | ftgs train stopped by SIGINT or SIGTERM, after saving its checkpoint |
Exit code 2 comes from the ValueErrors of the calls that read or check user input, and from the CLI's own checks; a ValueError anywhere else is a bug and keeps its traceback (Q-26, Q-142). Out of GPU memory inside Trainer.train is exit 2 as well: the step in progress is lost, so nothing is saved, and the message names the last iteration completed, the latest checkpoint (RUN_DIR/checkpoint, or the resumed one) and the remedy, a resume with perf.frame_cache = "mmap" or lower memory settings; out of memory anywhere else keeps its traceback. So is an OSError while writing a user-chosen output or making its folder: the init points, the renders, METRICS.json, the exchange model, the run directory and its config.json, train_log.jsonl, checkpoints and the profile trace. The message names the output, and the atomic writes leave the old file or checkpoint. A checkpoint save that fails during training saves nothing more (Q-159). A train_log.jsonl append that fails stops training after that iteration, as a signal does: the checkpoint is saved, the divergence check first, and the command exits 2 naming the log and the iteration the checkpoint holds, with no final evaluation. If that save fails too, the log's failure is the error and the save's a warning after it, and a diverged state is exit 3 still, the log's failure its warning (Q-159 as answered). A signal recorded in the same iteration keeps its exit code, the log's failure a warning, and a trace that couldn't be written at an earlier window's end is a warning before the log's error (Q-164). A trace that can't be written after training failed, a failed train_log.jsonl append included, is only a warning, so the training failure is the one reported (Q-160); otherwise training goes on, and the failure is reported once the run's checkpoint is saved, or as a warning when a signal stopped the run, whose exit code stands. In ftgs train's final evaluation one call both builds the LPIPS networks and writes METRICS.json, so its message names both (Q-161). An OSError raised by a read during training is not an output failure and keeps its traceback (Q-142 as answered); the safetensors library's write failures count as OSErrors (Q-158). Divergence is the trainer's FloatingPointError (Q-69). A render that isn't finite in an evaluation raises the same exception, from eval.evaluate_images before anything is written, in ftgs eval and through Trainer.evaluate in ftgs train (Q-129, Q-147).
ftgs train --out RUN_DIR writes (Q-132):
config.json, the resolved configuration, after--seed;train_log.jsonl, one row per log read: the iteration, the mean of each loss term since the last read, and N, the columns §3.3 asks of an adapter;checkpoint/, the latest save: when training reaches T_it, when a signal stops it, whentrain_log.jsonlcan't be written (Q-159 as answered), and with--checkpoint-every Nafter every iteration that is a multiple of N below T_it (none by default, D-83). The multiples are absolute iterations, so a resume rejoins the same schedule;checkpoint-<iteration>/, the earlier saves--keep-checkpoints Kkeeps: the K latest saves are kept, one per iteration,checkpoint/counting as one, the one before renamed as a new save arrives and the oldest beyond K removed once the new save is in place, so a save needs room for K + 1 checkpoints and one that fails leaves the K it had (Q-151). A save at iteration it first drops the kept checkpoints of it or later, the continuation a resume from an earlier one abandoned, and a save at the iterationcheckpoint/already holds replaces it in place. Only checkpoints are counted or removed (D-83, Q-149 to Q-152). With K > 1, acheckpoint-<n>that isn't a checkpoint, onto which a save would refuse to rename, is exit code 2 naming it, before the first iteration, with the run directory unchanged; one that appears during the run is exit code 2 at the save that would rename onto it, with no further save, and stops nothing otherwise (Q-150 as answered, Q-163, Q-159). Neither option is part of the configuration:config.json, the checkpoint's manifest and a resume's configuration check ignore them, and N < 0 or K < 1 is exit code 2;METRICS.json, the evaluation of the test split at the end, when the scene has one; it runs after the checkpoint is saved, so a failed evaluation loses no training, and a render that isn't finite writes no METRICS.json and exits 3 (Q-129);- after an out-of-memory failure in training (exit 2), only what was saved before it: the kept checkpoints,
config.jsonand the log so far; the message names the checkpoint to resume from withperf.frame_cache = "mmap"(Q-142 as answered, Q-134); profile/trace.json, with--profile START:COUNT: atorch.profilertrace of those iterations, which must lie inside the iterations the command runs; a window cut short still writes what it recorded (Q-144). Inside the window every stage of an iteration is a named range of the trace (iteration,data,slice,raster.forward, theloss.*terms,backward,statistics,adam,events,logand their sub-stages;docs/modules/train/trainer.md, "Stages"). The same stages are NVTX ranges in every run on a CUDA build of PyTorch, with or without--profile, so Nsight Systems and other external profilers show the same names. A trace grows with every iteration it holds, so long windows make large traces (Q-144): on CUDA about 1.7 MB and 5,100 trace events per iteration with the default configuration, whose perceptual loss runs LPIPS-VGG, and about 0.55 MB and 1,650 events with λ_perc = 0, with nothing fixed beyond that (Calibration data, 2026-10-19). A window of 1,000 iterations makes a trace of about 1.7 GB.
With perf.deterministic, ftgs train sets CUBLAS_WORKSPACE_CONFIG before CUDA starts, unless the environment already sets it (Q-131).
While training runs, SIGINT and SIGTERM are recorded rather than acted on at once. The trainer stops after the current iteration, so the saved state is whole (Q-69). The checkpoint is saved atomically (Q-25) and rotated like any other save (D-83), periodic saves keep signals waiting as the final save does, and the command exits with 128 plus the signal number (Q-133). A divergence saves nothing, so the last good checkpoint survives: the trainer checks at its log reads and events (Q-69), and the command checks the model once more before it saves, so a model that turned non-finite since is exit 3 too.
--resume CKPT continues the run on the checkpoint's own configuration. A --config or --seed given beside it must give that configuration, except the performance settings that change no number (slice_impl, packed, precull, frame_cache, prefetch, log_every, deterministic), which may differ: the resumed run uses the given ones, passes them to Trainer.from_checkpoint and records them in config.json and its checkpoints, and perf.deterministic sets CUBLAS_WORKSPACE_CONFIG as for a new run (Q-131). Any other difference, perc_precision, adam and images_per_step included, is exit 2 naming the dotted keys (Q-134 as answered, Q-153). A manifest that changed since the checkpoint was made needs --force (Q-70, Q-134). The run directory must be the run's own (its config.json differing from the configuration in those performance settings at most), or absent or empty. A signal that comes once the run has reached T_it doesn't stop it: the run is complete and goes on to its evaluation. D-24's check of the start budget F·B against N_max happens when the trainer builds the model, so ftgs init can't make it (Q-128).
ftgs eval MODEL SCENE.json renders the split (--split test, the default, or train; Q-143) with the model and scores it. A checkpoint is rendered against its configuration's background, an exchange model against the background its file records, black when it records none (D-84). With --renders-dir DIR it instead scores the renders in DIR (request.json and renders.safetensors), matching each image of the split to one request item by timestamp, image size and camera, the camera within a relative 1e-6 (Q-137). ftgs render --backend reference followed by ftgs eval --renders-dir scores the reference backend. Supplied renders are scored as given, so their request should use the background the model was trained with, which a checkpoint's configuration and an exchange file's render_conventions record (Q-141, D-84): renders made on black of a model trained on white score that mismatch, which ftgs eval can't detect from the files. A split with masks on some images but not all is refused (Q-135). METRICS.json names the model by the SHA-256 of its file, or of its checkpoint's manifest.json, taken before and after the model is read: a model that changed meanwhile, as RUN_DIR/checkpoint does at a periodic save of a run in progress, is exit 2 (Q-136 as answered, Q-154). The command says the model changed while it was read whenever a save lands inside the read: when the second hash can't be taken, the model being gone, and when the read fails and the hash then differs from the first or can't be taken. A read that fails with the hash unchanged keeps the reader's message, so a damaged checkpoint is still reported as damaged, and a model absent before the first hash is reported missing (Q-154 as answered). A render that isn't finite, NaN or ±Inf in its colors or alpha, is exit 3, naming up to five images, and no METRICS.json is written: E16's clamp would otherwise score an Inf render as a plausible image. A null in METRICS.json therefore always means "not defined here" (a perfect render's PSNR, a mean over no images), never a failed render. A supplied renders file that isn't finite is refused by read_renders, exit 2, since the file is the input (Q-129).