Profiling¶
Speed measurement tools for data pipelines, models, and training loops, plus GPU trial-packing probes.
DataProfiler ¶
Records per-batch data loading and training step timings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stall_threshold
|
float
|
seconds above which a data wait counts as a stall |
0.005
|
Source code in tsfast/training/profiling.py
data_wait_fraction
property
¶
Fraction of total time spent waiting for data.
summary ¶
Formatted profiling summary with actionable diagnostics.
Source code in tsfast/training/profiling.py
profile
classmethod
¶
Instrument a Learner's training loop to measure data vs step time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
learner
|
the Learner to instrument |
required | |
stall_threshold
|
float
|
seconds above which a data wait counts as a stall |
0.005
|
Source code in tsfast/training/profiling.py
ConfigProbe
dataclass
¶
ConfigProbe(config: dict, reserved_bytes: int, footprint_bytes: int, step_time_s: float | None = None, busy_fraction: float | None = None, power_fraction: float | None = None)
Measurements for one sampled config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
dict
|
the sampled hyperparameter dict |
required |
reserved_bytes
|
int
|
peak torch caching-allocator reservation during training steps |
required |
footprint_bytes
|
int
|
|
required |
step_time_s
|
float | None
|
steady-state seconds per training step (busy subset only) |
None
|
busy_fraction
|
float | None
|
mean NVML utilization over the steady-state window (over-reads for launch-bound models: it counts "any kernel in the sample window") |
None
|
power_fraction
|
float | None
|
mean power draw / enforced power limit (under-reads; the two bracket the true saturation) |
None
|
SaturationProbe
dataclass
¶
SaturationProbe(per_config: list[ConfigProbe], total_mem_bytes: int, context_overhead_bytes: int, device: str, driver_version: str | None = None)
Result of :func:probe_gpu_saturation; input to :func:recommend_trials_per_gpu.
Footprints are conservative upper estimates: configs are probed sequentially in one
process, so caches warmed by earlier configs (cuDNN workspaces, Triton autotune) can
inflate later readings, and the max sampled footprint is not a guaranteed supremum
over the search space. The per-trial quota (:func:tsfast.tune.apply_gpu_quota)
keeps that safe: a config fails deterministically against its own slice instead of
taking down neighbors.
footprint_ratio
property
¶
max/median footprint — below ~2 a uniform k loses little to config-size spread.
PackingRecommendation
dataclass
¶
PackingRecommendation(k: int, k_mem: int, k_compute: int | None, k_compute_conservative: int | None, quota_fraction: float, context_overhead_bytes: int, mem_margin: float)
How many concurrent trials one GPU supports for the probed workload.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
recommended trials per GPU — |
required |
k_mem
|
int
|
hard memory ceiling from the footprint quantile |
required |
k_compute
|
int | None
|
saturation prior from power fraction (optimistic side; None without NVML).
A prior, not ground truth — it ignores bandwidth/L2 contention; validate anchors
with :func: |
required |
k_compute_conservative
|
int | None
|
saturation prior from NVML utilization (pessimistic side: utilization over-reads for launch-bound models, so this can read k=1 on exactly the small models that pack best) |
required |
quota_fraction
|
float
|
per-process allocator fraction for |
required |
context_overhead_bytes
|
int
|
per-process CUDA context overhead carried over from the probe |
required |
mem_margin
|
float
|
fraction of device memory budgeted (headroom for fragmentation) |
required |
PackingCurve
dataclass
¶
Measured aggregate-throughput packing curve from :func:measure_packing_curve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aggregate
|
dict[int, float]
|
|
required |
per_worker
|
dict[int, list[float]]
|
|
dict()
|
benchmark_dataloaders ¶
benchmark_dataloaders(dls_factory: Callable, num_workers_list: list[int] | None = None, n_batches: int = 100, **factory_kwargs) -> dict[int, dict[str, float]]
Benchmark data loading speed across different num_workers values.
Creates DataLoaders with each num_workers setting, iterates n_batches,
and reports throughput.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dls_factory
|
Callable
|
callable that returns a DataLoaders (e.g. |
required |
num_workers_list
|
list[int] | None
|
values to test (default |
None
|
n_batches
|
int
|
batches to iterate per setting |
100
|
**factory_kwargs
|
forwarded to dls_factory |
{}
|
Source code in tsfast/training/profiling.py
time_inference ¶
time_inference(model: Module, xb: Tensor, devices: tuple[str, ...] = ('cpu', 'cuda'), n_warmup: int = 3, min_seconds: float = 2.0) -> dict[str, dict[str, float]]
Measure forward-pass speed of a model on one batch, per device.
The model is deep-copied per device, so the caller's model is untouched. Unavailable devices are skipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
model to time |
required |
xb
|
Tensor
|
sample input batch |
required |
devices
|
tuple[str, ...]
|
device names to measure on |
('cpu', 'cuda')
|
n_warmup
|
int
|
untimed iterations before measurement (absorbs cuDNN autotune, lazy init) |
3
|
min_seconds
|
float
|
measurement time budget per device |
2.0
|
Returns:
| Type | Description |
|---|---|
dict[str, dict[str, float]]
|
|
Source code in tsfast/training/profiling.py
time_training_module ¶
time_training_module(model: Module, xb: Tensor, yb: Tensor, loss_fn: Callable = F.mse_loss, devices: tuple[str, ...] = ('cpu', 'cuda'), n_warmup: int = 3, min_seconds: float = 2.0) -> dict[str, dict[str, float]]
Measure raw training-step speed (forward + loss + backward + SGD step), per device.
Times pure model compute on a fixed batch — no dataloader, transforms, or
TBPTT chunking. For real training-loop speed use :func:time_training_learner;
the gap between the two is pipeline overhead. SGD is used regardless of how
the model will actually be trained, so optimizer state does not affect timing.
The model is deep-copied per device, so the caller's model is untouched.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
model to time |
required |
xb
|
Tensor
|
sample input batch |
required |
yb
|
Tensor
|
sample target batch |
required |
loss_fn
|
Callable
|
loss |
mse_loss
|
devices
|
tuple[str, ...]
|
device names to measure on |
('cpu', 'cuda')
|
n_warmup
|
int
|
untimed iterations before measurement (absorbs cuDNN autotune, lazy init) |
3
|
min_seconds
|
float
|
measurement time budget per device |
2.0
|
Returns:
| Type | Description |
|---|---|
dict[str, dict[str, float]]
|
|
Source code in tsfast/training/profiling.py
time_training_learner ¶
Measure real training-loop speed and extrapolate epoch time.
Runs the learner's actual training loop — dataloader, transforms, augmentations, TBPTT chunking — for a few batches on the learner's own device. Model weights and optimizer state are restored afterwards, so the learner is left untrained. The extrapolation slightly underestimates true epoch time since validation and per-epoch overheads are not measured.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
learner
|
Learner to measure |
required | |
n_batches
|
int
|
number of timed batches (dataloader is re-iterated if shorter) |
20
|
n_warmup
|
int
|
untimed batches before measurement |
3
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
|
Source code in tsfast/training/profiling.py
sample_ray_space ¶
Draw n random configs from a Ray Tune declarative search space.
Plain values in space pass through as constants; nested dicts are sampled
recursively. tune.sample_from entries depend on other resolved values and
cannot be sampled standalone — pass explicit configs to the probe instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
space
|
dict
|
dict mapping keys to |
required |
n
|
int
|
number of configs to draw |
20
|
seed
|
int
|
seed for reproducible sampling |
0
|
Source code in tsfast/training/profiling.py
sample_optuna_space ¶
Draw n random configs from an Optuna define-by-run space function.
Runs space_fn against throwaway trials from a RandomSampler study. If
space_fn returns a dict it is used directly; otherwise the config is read
from trial.params (the usual define-by-run style, where the function only
calls trial.suggest_*).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
space_fn
|
Callable
|
|
required |
n
|
int
|
number of configs to draw |
20
|
seed
|
int
|
seed for the RandomSampler |
0
|
Source code in tsfast/training/profiling.py
probe_gpu_saturation ¶
probe_gpu_saturation(make_learner: Callable, configs: list[dict], n_steps_mem: int = 5, n_steps_busy: int = 50, warmup: int = 10, busy_subset: int = 3, device=None) -> SaturationProbe
Measure memory footprint and GPU saturation of a workload's config distribution (packing step 1: measure).
Per config: builds the learner, runs a few real training steps
(learner.training_step, so TBPTT chunking and transforms are reflected), and
records the peak allocator reservation plus the CUDA context overhead measured via
NVML per-process memory. For busy_subset configs nearest the median footprint it
additionally samples NVML utilization and power over a steady-state window.
Intended workflow: run this across the whole model x dataset grid, validate a few
anchor pairs with :func:measure_packing_curve (tier 2), freeze the numbers.
A probe samples the space at probe time — a TPE/BO search may concentrate on larger
configs later; the per-trial quota keeps that safe, the throughput estimate may drift.
Without NVML the probe degrades to memory-only (context overhead 0, no busy/power).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
make_learner
|
Callable
|
factory |
required |
configs
|
list[dict]
|
configs to probe; from :func: |
required |
n_steps_mem
|
int
|
max training steps per config for the footprint (stops early once the peak reservation is stable; optimizer state and workspaces appear within the first steps) |
5
|
n_steps_busy
|
int
|
training steps in the timed steady-state window |
50
|
warmup
|
int
|
untimed steps before the busy window (absorbs autotune and lazy init) |
10
|
busy_subset
|
int
|
number of configs to measure busy/power on (0 disables) |
3
|
device
|
target CUDA device (defaults to the current one) |
None
|
Source code in tsfast/training/profiling.py
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 | |
recommend_trials_per_gpu ¶
recommend_trials_per_gpu(probe: SaturationProbe, mem_margin: float = 0.9, max_k: int = 8, footprint_quantile: float = 1.0) -> PackingRecommendation
Derive a trials-per-GPU recommendation from a :class:SaturationProbe (packing step 2: decide).
Pure math on the probe result — instant and GPU-free, so it can be re-run under different margins or budgets without re-measuring.
Memory is a feasibility constraint (unused VRAM has no performance value), so
k_mem is a hard ceiling; the compute priors only cap k below it when the single
trial already saturates the device. Footprints include the CUDA context, so k_mem
bounds true device usage of k co-located processes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
probe
|
SaturationProbe
|
measurements from :func: |
required |
mem_margin
|
float
|
fraction of device memory to budget |
0.9
|
max_k
|
int
|
upper clip for the recommendation |
8
|
footprint_quantile
|
float
|
size |
1.0
|
Source code in tsfast/training/profiling.py
measure_packing_curve ¶
measure_packing_curve(make_learner: Callable, configs: list[dict], ks: tuple[int, ...] = (1, 2, 4), warmup: int = 10, measure_seconds: float = 30.0, device=None) -> PackingCurve
Measure ground-truth aggregate throughput of k co-located training processes (packing step 3: verify).
For each k spawns k separate worker processes on one device (threads would serialize kernel launches on the GIL and CUDA streams share one allocator — both overstate capacity relative to real multi-process co-location). Each worker builds its learner, warms up its own config (so Triton/cuDNN autotune stays out of the timed window), rendezvouses at a barrier, then counts training steps inside a shared wall-clock window — staggered process startup therefore cannot inflate the aggregate.
This is the validator (and no-NVML fallback) for the tier-1 heuristic: run it on a
few anchor configs and compare :attr:PackingCurve.knee against the recommendation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
make_learner
|
Callable
|
factory |
required |
configs
|
list[dict]
|
picklable configs, drawn round-robin across workers so the mix reflects a real tuning session |
required |
ks
|
tuple[int, ...]
|
co-location levels to measure |
(1, 2, 4)
|
warmup
|
int
|
untimed steps per worker before the barrier |
10
|
measure_seconds
|
float
|
length of the timed window |
30.0
|
device
|
target CUDA device (defaults to the current one) |
None
|
Source code in tsfast/training/profiling.py
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 | |