GPU acceleration¶
ml4 can push tree-booster training onto the GPU and run torch-backed families (foundation models, neural nets, sentence embeddings) on CUDA. GPU is off by default — enabling it is one runtime knob, plus a CUDA torch build for the torch-backed paths.
In short
execution.deviceis"cpu"by default;"auto"uses a GPU where each booster library can take one and falls back to CPU otherwise;"cuda"refuses to start if one cannot.- ml4 probes each library (XGBoost, LightGBM, CatBoost) once per process, and the GPU decision applies to the whole roster: one library short keeps every booster on CPU.
- Torch-backed families (foundation models, neural nets, embeddings) need a CUDA torch build.
- CatBoost trains on CPU when monotone constraints are active.
- GPU runs are not bit-reproducible; train on CPU when exactness matters.
Enabling GPU for boosters¶
ExecutionConfig.device selects the device for the tree boosters
(LightGBM / XGBoost / CatBoost). The three values differ in what happens when the
GPU turns out not to be usable:
execution.device |
GPU present & usable | GPU absent or unusable |
|---|---|---|
"cpu" (default) |
CPU — no GPU code path is exercised at all | CPU |
"auto" |
GPU, per library | quietly falls back to CPU, logged once per library |
"cuda" |
GPU, per library | raises, naming the library and how to install a GPU build |
from parleml_ml4 import Ml4Config
cfg = Ml4Config()
cfg.data.target_column = "y"
cfg.execution.device = "auto" # opportunistic: GPU where available, CPU otherwise
cfg.execution.device = "cuda" # insist: abort now rather than train on CPU
Why cuda refuses instead of degrading¶
"cuda" is not an alias for "auto". It serves one case: you provisioned a GPU box,
you are paying for it, and a four-hour run that silently drops to CPU is a worse failure
mode than a clean abort in the first second. "auto" keeps the opportunistic behaviour for
a config that must run anywhere; "cuda" is the assertion, and it fails with the install
hint for the library that could not take the device.
How the probe decides¶
Capability is answered per library, because "is CUDA available" is a different
question for each of them and getting it wrong in either direction is expensive. Every
probe is lazy and cached — it runs at most once per process, and device = "cpu"
short-circuits before any of them, so a CPU run never imports a GPU stack or opens a
CUDA context:
| Library | How availability is established |
|---|---|
| XGBoost | xgboost.build_info() reports the compile-time USE_CUDA / USE_HIP flags — free, and touches no device |
| LightGBM | GPU support is fixed at cmake time with no introspection API at all, so ml4 genuinely trains: one iteration on four rows, OpenCL first, then CUDA |
| CatBoost | catboost.utils.get_gpu_device_count() — the driver query through the same runtime CatBoost will train with |
| all of them | plus AutoGluon's own GPU count, deliberately reused rather than counted independently: it is the number AutoGluon asserts and clamps against |
Each decision is logged once per process — the boosters' resolution and the fit-level outcome both — so the log says what the run actually did without repeating it per model.
The GPU decision is all-or-nothing across the roster
A roster of ["GBM:gbdt", "XGB:gbtree"] on a host whose LightGBM is CPU-only trains
both on CPU. Narrow roster.models to the libraries your host can actually
accelerate, rather than expecting a mixed roster to split across devices.
Details: why one library short keeps the whole fit on CPU
AutoGluon's fit-level num_gpus is a total for the run, applied to every model
that does not override it, and ml4 does not compute a per-library allocation from the
probe. So one library short takes the whole fit off the GPU: granting it would hand
num_gpus=1 to a library that cannot use it, and a CPU-only LightGBM handed a GPU
does not fall back — AutoGluon sets device="gpu" and the fit dies inside
lgb.train.
This is a design choice, not a missing capability. A roster entry can override the
fit-level total (ag_args_fit={"num_gpus": 0}), and ml4 uses exactly that for the
CatBoost + monotone rule below. What the probe does not do is turn its per-library
verdict into per-library overrides, because the device decision is taken in the config
translator, which never sees the roster entries.
The resolved fit-level request is one of three values, and the third one matters:
num_gpus |
When | Effect |
|---|---|---|
0 |
device = "cpu" |
Pinned off, so a present GPU cannot be grabbed by a model whose own default would have taken one |
1 |
every GPU-capable family in the roster resolved to a real backend | The boosters train on GPU |
"auto" |
device asked for a GPU but the boosters could not take it |
AutoGluon's per-model defaults are left alone, so the torch-backed families still take a GPU when torch sees one |
That last row is deliberate: pinning 0 there would silently demote the foundation
challenger, which is not what a user asking for a GPU meant.
Details: why ml4 probes instead of forcing num_gpus=1
AutoGluon's tabular boosters resolve num_gpus="auto" to zero GPUs by their own
default, so a GPU run needs an explicit count. But AutoGluon asserts the requested GPU
count against the system's, so an unconditional num_gpus=1 errors on a GPU-less host —
and AutoGluon does not probe the LightGBM build either (its own source concedes the gap).
The probe is the precondition that makes the request safe to emit.
Two GPU paths ml4 deliberately refuses¶
Both are cases where the device is genuinely available and using it would still be
wrong. Under "auto" each logs and falls back; under "cuda" each raises.
- A CUDA-only LightGBM build. LightGBM ships two mutually exclusive GPU backends —
OpenCL (
device_type="gpu") and its native CUDA learner. AutoGluon only ever asks for OpenCL, so ml4 cannot reach a CUDA-only build's learner; claiming the GPU would handlgb.traina backend the build does not have. The probe still reports what it found so the refusal can say why. - A tuned CatBoost. ml4's hyperparameter search scores
its trials on CPU-pinned fits, so the space it searches offers CPU-only values —
MVSbootstrap, which GPU CatBoost rejects outright, andrsm, which CatBoost silently ignores on GPU (making the tuned value a lie about the fitted model). WithCATin the roster andhpo.provider="ml4_optuna", the fit therefore stays off the GPU.
Thread oversubscription
execution.n_jobs = -1 uses all CPUs for sklearn-style estimators. Booster
libraries read it too but their internal thread pools may still
oversubscribe on shared hosts — pin OMP_NUM_THREADS if that matters.
Foundation & torch-backed families¶
Foundation models (TabICL) and other torch-backed families
run on whatever device torch sees, and ml4 deliberately emits no per-model GPU override
for them — an explicit num_gpus=1 errors outright on a GPU-less host. They are also
not gated by the booster probe: when the boosters fall back, the fit-level request
stays "auto" rather than being pinned to 0, so a torch family still takes the GPU
that torch can see. device = "cpu" is the one setting that pins everything off. The
only per-model device override ml4 emits is CatBoost→CPU under monotone constraints
(below). See Tabular foundation models for the catalogue and the
foundation_ensemble preset.
Which torch you get depends on where it comes from. From PyPI, pip install parleml-ml4
brings the CPU build on Windows and the CUDA build on Linux (with several GB of NVIDIA
libraries). The ParleML image and the repository's workspace pin the CPU build. Choose the build
by installing torch from PyTorch's own index before ml4:
# CPU-only (Linux): keeps the NVIDIA libraries out
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install "parleml-ml4[mlops]"
# GPU torch for foundation / embedding backends (Windows, or a specific CUDA version)
pip install torch --index-url https://download.pytorch.org/whl/cu124
The CatBoost + monotone CPU rule¶
There is one hard incompatibility: CatBoost honours monotone constraints on CPU
only. ml4 handles it with a single, non-fatal per-model override: when the recipe
carries an active monotone direction (binary / regression) and the run is not
regulated, fit_predictor puts ag_args_fit={"num_gpus": 0} on CatBoost's roster
entry even under a global GPU device, while every other model keeps the GPU. The run
continues, the constraint binds, and the run's notifications carry a CatBoost trains on
CPU warning, once per CatBoost roster entry:
CAT:symmetric is pinned to CPU (num_gpus=0) because monotone constraints are active and
CatBoost honours them on CPU only, so it trains slower than the run's GPU allows.
LightGBM and XGBoost honour monotone constraints on GPU, so a monotone GBM/XGB roster is unaffected.
Regulated runs are not refused
A regulated run trains one transparent model (glm / scorecard / rulefit / ebm)
and never CatBoost, so monotone constraints on a GPU device need no refusal there. A
regulated EBM with monotone constraints trains normally on a GPU workstation. The
CPU pin above is the whole mechanism.
What GPU does and does not cover¶
| Workload | GPU support |
|---|---|
| LightGBM / XGBoost / CatBoost training | via execution.device, subject to the per-library probe (CatBoost CPU-only under monotone, and under hpo) |
| Foundation models (TabICL) | via CUDA torch build |
| Neural / transformer families (NN_TORCH, FT-Transformer, …) | via CUDA torch build |
| Sentence-transformer embeddings | via CUDA torch build |
| Post-fit layers (calibration, thresholds, intervals) | CPU (cheap, not a bottleneck) |
Verifying the GPU path
Start with execution.device = "auto" and read the once-per-library decision in the
run log. If you would rather find out in second one than in hour four, set
execution.device = "cuda" and let the run refuse. For torch backends, confirm
torch.cuda.is_available() in your serving environment — a CPU torch build reports
False regardless of execution.device.
GPU training is not bit-reproducible
CUDA reduces floats non-deterministically, so a GPU run does not reproduce
bit-for-bit even at a fixed execution.seed. Train on CPU when exactness matters —
see Reproducibility & seeds.
See Troubleshooting for install-gating errors and Signal check for the GPU-friendly zero-shot probe.