Profiling and timers
TensorKit's index manipulations, tensor contractions and factorizations are instrumented with TimerOutputs.jl sections that are compiled away by default, so they incur no runtime cost. They can be enabled to obtain a detailed breakdown of where time is spent inside these operations, in particular the split between the different kinds of work involved in manipulating symmetric tensors:
| category | contents |
|---|---|
symmetry | fusion tree manipulations and recoupling coefficients (braiding, transposing, F- and R-symbols) |
bookkeeping | block structure computations, cache lookups, contraction planning |
alloc | allocation of output tensors and temporary buffers |
dense | dense tensor kernels (BLAS/LAPACK calls, strided permutations and additions) |
other | remaining time within an instrumented operation (dispatch, argument checking, uncovered overhead) |
The canonical workflow looks as follows:
using TensorKit
TensorKit.enable_timers!() # triggers recompilation of the instrumented methods
# warm up first, so that compilation does not pollute the timings
V = SU2Space(0 => 4, 1//2 => 4, 1 => 2)
t = rand(V ⊗ V ← V ⊗ V)
permute(t, ((1, 3), (2, 4)))
@tensor t2[a b; c d] := t[a x; c y] * t[y b; x d]
svd_compact(t)
TensorKit.reset_timers!()
# ... run the workload of interest ...
TensorKit.print_timers() # full nested call tree
TensorKit.timer_summary() # symmetry / bookkeeping / alloc / dense / other totals
TensorKit.disable_timers!()TensorKit.print_timers displays the accumulated timings as a nested call tree, with sections for the top-level operations ("permute!/braid!", "contract!", "svd_compact!", ...) and nested sections labeled by their category prefix ("symmetry: ...", "bookkeeping: ...", "alloc: ...", "dense: ..."). TensorKit.timer_summary aggregates the exclusive time of each section (its own time minus that of its timed children) into per-category totals, such that every nanosecond is counted exactly once and the totals sum to the total measured time.
A few caveats to keep in mind:
- Enabling or disabling the timers redefines internal functions, so instrumented methods recompile on first use afterwards. This is a debug-session switch, not a runtime option.
- While timers are enabled, TensorKit-internal task parallelism is disabled, since the timer object may only be manipulated from a single task. As a consequence, multi-threaded speedups are not measurable while timing, and TensorKit functions should not be called concurrently from multiple user tasks.
- Each section entry costs roughly 100–200 ns, which can distort measurements of workloads on very small tensors.
- The construction of fusion tree transformers and block structures is cached (see
empty_globalcaches!), so their cost only shows up the first time a given structure is encountered. CallTensorKit.empty_globalcaches!()before the measurement if you want the construction cost to be included, or after the warm-up if you want to measure the steady-state behavior with warm caches. - For GPU tensors, the timings only reflect host-side dispatch of asynchronous kernels, unless the workload is explicitly synchronized.
Library documentation
TensorKit.GLOBAL_TIMER — Constant
TensorKit.GLOBAL_TIMERThe global TimerOutput object into which all timer sections of TensorKit accumulate. See enable_timers! for how to activate them, print_timers for displaying the resulting call tree and timer_summary for aggregating it into per-category totals.
TensorKit.enable_timers! — Function
TensorKit.enable_timers!()Enable all timer sections of TensorKit (including its submodules), which accumulate timings of the internal kernels into GLOBAL_TIMER. Undone by disable_timers!.
Enabling or disabling timers redefines internal functions and therefore triggers recompilation of the instrumented methods on first use. This is a debug-session operation, not a runtime switch.
While timers are enabled, TensorKit-internal task parallelism is disabled (threaded regions run serially), so multi-threaded speedups are not measurable. Additionally, TensorKit functions should not be called concurrently from multiple user tasks while timing, as the timer object is not thread-safe.
Note that each timer section adds an overhead of roughly 100-200 ns per entry, which can distort measurements of very small workloads. For GPU tensors, timings only reflect host-side dispatch of asynchronous kernels unless the workload is explicitly synchronized.
TensorKit.disable_timers! — Function
TensorKit.disable_timers!()Disable all timer sections of TensorKit again; the inverse of enable_timers!. Also triggers recompilation of the instrumented methods on first use.
TensorKit.reset_timers! — Function
TensorKit.reset_timers!()Reset the accumulated timings in GLOBAL_TIMER.
TensorKit.print_timers — Function
TensorKit.print_timers(io::IO = stdout; kwargs...)Print the accumulated timings in GLOBAL_TIMER as a nested table. Keyword arguments are forwarded to TimerOutputs.print_timer.
TensorKit.timer_summary — Function
TensorKit.timer_summary([io::IO]; to = GLOBAL_TIMER)
-> Dict{Symbol, @NamedTuple{time::Int64, allocated::Int64, ncalls::Int64}}Aggregate the timer tree into per-category totals (time in ns, allocated bytes, number of section entries) for the categories (:symmetry, :bookkeeping, :alloc, :dense, :other).
Each section's exclusive time (its own time minus that of its timed children) is attributed to the category given by its label prefix or, for unprefixed labels, to the category of the nearest categorized ancestor (:other at the root). Every nanosecond is thus counted exactly once and the totals sum to the total measured time.
When io is given (default stdout), a small table is printed; pass nothing to skip printing and only return the totals.
TensorKit.timer — Function
TensorKit.timer() -> TimerOutputs.TimerOutputReturn the global timer object GLOBAL_TIMER into which all TensorKit timer sections accumulate.
TensorKit.timers_enabled — Function
TensorKit.timers_enabled() -> BoolReturn whether the @timeit_debug timer sections of TensorKit are currently compiled in, i.e. whether enable_timers! has been called.
This is a documented alias for timeit_debug_enabled, the switch that is redefined by TimerOutputs.enable_debug_timings. It is used internally to force serial execution of parallel regions while timing, since a TimerOutput may only be manipulated from a single task. When timers are disabled this check const-folds to false, so it has no runtime cost.