Autotuning and Heuristics

Kernel performance may be significantly impacted by implementation-specific hyperparameters (e.g., tile sizes, layouts, etc.), known as Op Configs in Tokamax. The optimal values for these configs must be empirically determined for every input of interest, via autotuning. Tokamax provides a framework both to perform autotuning, and to simplify the management of this process and its outputs.

High-level Overview

Every tokamax.Op implementation defines its set of tunable hyperparameters, and the range of values each of those configs can assume, in tokamax.Op._get_autotuning_configs (example). The autotuner runs an exhaustive search across this space to identify the configuration with the best performance (lowest execution time). The result may be utilized in different ways: directly in your program as a context manager, serialized and stored in your private cache for later use, or as part of the global library-wide cache.

The optimal config for an op depends on the specific set of inputs used to invoke the op, including shapes, dtypes, etc. The autotuning API accepts this list of inputs in a variety of ways, and in all cases, outputs an AutotuningResult object that contains the best configurations for each op in the input.

Autotuner Input Formats

Callable Function

Given a callable function of interest with one or more Tokamax Ops, the autotuner uses StableHLO metadata to extract information about every Op and its respective inputs, and autotunes each one.

# Autotune all Tokamax kernels in f, which can be a JAX function with multiple Tokamax ops and non-Tokamax ops as well. Assume f takes a dictionary of args as input.
autotune_result: tokamax.AutotuningResult = tokamax.autotune(f, **args)
# Best possible result
with autotune_result:
    out_autotuned = f(**args)

Tokamax VJP ops may be implemented as their own Op class and have their own search space and unique _get_autotuning_configs. Since the VJP ops do not have a direct API that can be called for autotuning, you can take your forward function and call jax.grad to obtain the pullback function with Tokamax VJP ops, and then pass the pullback function into the autotuner.

# This pullback function contains Tokamax VJP ops.
f_grad = jax.grad(f)
autotune_result = tokamax.autotune(f_grad)

with autotune_result:
  out = f_grad()

Sequence of BoundArguments

When a Tokamax Op is called with a set of input arguments, the op will first "bind" to the arguments. This process canonicalizes and validates the inputs, creating a BoundArgument object consisting of the op with its input arguments. The autotuner can take in a sequence of BoundArguments as input, and autotunes each one.

ragged_dot_ba = tokamax.PallasMosaicTpuRaggedDot.bind(x, y, group)
attention_ba = tokamax.PallasMosaicTpuAttention.bind(q, k, v)

autotune_result: tokamax.AutotuningResult = tokamax.autotune([ragged_dot_ba, attention_ba])

with autotune_result:
  ...

Serialized List of BoundArguments

The script xplane_to_bound_args.py takes an xplane proto, generated by XProf during profiling runs, as input and outputs a JSON of BoundArguments. The autotuner can read in this JSON file and autotune all the Tokamax Ops that appear in the profile.

Suppose from profiling you have an xplane my_model.xplane.pb, you can extract and autotune all the Tokamax ops used like this:

 python tokamax/_src/tools/xplane_to_bound_args.py \
   --xplane_file=my_model.xplane.pb \
   --output_file=/tmp/bound_args.json
import tokamax

bound_args = tokamax.autotuning.bound_args_from_json("/tmp/bound_args.json")
autotune_result = tokamax.autotune(bound_args)

Autotuner Output Usage

The autotuning process always returns an AutotuningResult object that contains the best configurations for each op in the input. You may utilize that output in different ways to suit your needs.

Context Manager

You may directly utilize the autotuning result as a context manager in your code.

# Autotune all Tokamax kernels in f, which can be a JAX function with multiple Tokamax ops and non-Tokamax ops as well. Assume f takes a dictionary of args as input.
autotune_result: tokamax.AutotuningResult = tokamax.autotune(f, **args)
# Best possible result
with autotune_result:
    out_autotuned = f(**args)

Serialized AutotuningResult

You may wish to serialize and reuse autotune_result for one of two reasons (1) autotuning can be expensive in terms of compute time, depending on the size of the search space (2) kernel execution times can be noisy, resulting in different “best” choices in different runs, and may result in numerical non-determinism.

autotune_result: tokamax.AutotuningResult = tokamax.autotune(f, **args)
autotune_result_json: str = autotune_result.dumps()
autotune_result = tokamax.AutotuningResult.loads(autotune_result_json)
with autotune_result:
    out_autotuned = f(**args)

Multiple AutotuningResult objects can also be merged together into a single object, by reading the results in as a JSON string using load.

autotune_result_1 = tokamax.AutotuningResult.load(path_to_file_1)
autotune_result_2 = tokamax.AutotuningResult.load(path_to_file_2)
merged_results = autotune_result_1 | autotune_result_2

with merged_results:
  ...