Source code for evox.triton_kernels.kernels.lora_noise

"""LoRA (Low-Rank Adaptation) noise utility functions.

These pure-PyTorch helpers build on :mod:`~evox.triton_kernels.kernels.philox`
to generate deterministic, non-overlapping random low-rank perturbation streams.
They are **not** Triton kernels themselves — they call
:func:`~evox.triton_kernels.kernels.philox.philox_normal` internally, which
dispatches to Triton on CUDA and to the PyTorch fallback elsewhere.

The functions support a ``batched / population`` first dimension so that, given
``pop_size`` seeds, one call produces ``pop_size`` independent LoRA factor
samples. This is useful for population-based (evolutionary) perturbation of
neural-network weights, where each individual gets its own low-rank delta.

Key ideas:

- :func:`compute_counter_offsets` partitions the Philox counter space so that
  different parameter blocks (or A/B sub-factors within a block) draw from
  non-overlapping streams.
- :func:`generate_lora_factors` produces the per-individual low-rank factors
  ``(A, B)`` (or a flat noise vector for 1-D weights).
- :func:`lora_delta_output` applies the low-rank delta to an input without ever
  materializing the full ``(out_features, in_features)`` weight delta.
- :func:`lora_gradient` estimates the gradient of a population objective with
  respect to a weight, given per-individual fitness and factors.
"""

import torch

from .philox import philox_normal


[docs] def _flatten_to_2d(weight_shape: tuple) -> tuple[int, int]: """Flatten a weight shape to ``(d, k)`` where ``k`` is the last dim. ``d`` is the product of all dimensions except the last; ``k`` is the last dimension. For a 1-D shape this returns ``(1, n)`` — but callers handle the 1-D case separately, so this is only invoked for ``len >= 2``. :param weight_shape: The original weight tensor shape. :return: ``(d, k)`` where ``d = prod(shape[:-1])`` and ``k = shape[-1]``. """ d = 1 for s in weight_shape[:-1]: d *= s k = weight_shape[-1] return d, k
[docs] def _ceil_div4(x: int) -> int: """Round ``x`` up to the next multiple of 4 (Philox yields 4 values per call). :param x: An element count. :return: ``ceil(x / 4) * 4``. """ return ((x + 3) // 4) * 4
[docs] def compute_counter_offsets(param_shapes: list[tuple], lora_rank: int) -> list[int]: """Compute non-overlapping Philox counter offsets for a list of parameter blocks. For each parameter block we compute how many Philox elements it consumes, then round up to a multiple of 4 (Philox produces 4 values per call). The offsets are cumulative so every sub-stream occupies a disjoint counter range. - **1-D block** ``(n,)``: consumes ``n`` elements. - **2-D or >2-D block** ``(d, k)`` (flattened): consumes ``lora_rank * k`` (factor ``A``) plus ``d * lora_rank`` (factor ``B``). :param param_shapes: List of weight shapes (each a tuple of ints). :param lora_rank: The LoRA rank ``r``. :return: A list of starting counter values, one per block (cumulative). """ offsets = [] cur = 0 for shape in param_shapes: offsets.append(cur) if len(shape) == 1: n_elements = shape[0] else: d, k = _flatten_to_2d(shape) n_elements = lora_rank * k + d * lora_rank cur += _ceil_div4(n_elements) return offsets
[docs] def generate_lora_factors( seeds: torch.Tensor, weight_shape: tuple, rank: int, counter: int, ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: """Generate deterministic LoRA factors for a batch of individuals. - **1-D weight** ``(n,)``: returns a flat noise tensor of shape ``(pop_size, n)`` drawn from :func:`philox_normal`. - **2-D / >2-D weight** ``(d, k)`` (flattened, ``d = prod(shape[:-1])``, ``k = shape[-1]``): returns a tuple ``(A, B)`` where - ``A`` has shape ``(pop_size, rank, k)`` (the "down" projection), - ``B`` has shape ``(pop_size, d, rank)`` (the "up" projection), drawn from two non-overlapping Philox sub-streams. The product ``B @ A`` reconstructs a low-rank delta of shape ``(d, k)`` per individual. :param seeds: 1-D ``int64`` tensor of per-individual seeds. :param weight_shape: The target weight tensor shape. :param rank: The LoRA rank ``r``. :param counter: Starting Philox counter for this block. :return: Flat noise tensor (1-D weight) or ``(A, B)`` tuple (≥2-D weight). """ pop_size = seeds.shape[0] if len(weight_shape) == 1: n = weight_shape[0] noise = philox_normal(seeds, n, counter) return noise else: d, k = _flatten_to_2d(weight_shape) # Factor A: (rank, k) per individual — "down" projection. a_elements = rank * k a_offset = counter # Factor B: (d, rank) per individual — "up" projection, drawn from a # non-overlapping counter range immediately after A's stream. b_offset = counter + _ceil_div4(a_elements) b_elements = d * rank A = philox_normal(seeds, a_elements, a_offset).reshape(pop_size, rank, k) B = philox_normal(seeds, b_elements, b_offset).reshape(pop_size, d, rank) return (A, B)
[docs] def lora_delta_output( x: torch.Tensor, A: torch.Tensor, B: torch.Tensor, sigma: float, ) -> torch.Tensor: """Apply a batched low-rank delta to an input without materializing the weight. Given a batched input ``x`` and per-individual LoRA factors ``(A, B)``, compute ``sigma * (x @ Aᵀ) @ Bᵀ``. This is mathematically equivalent to ``sigma * x @ (B @ A)ᵀ`` (where ``B @ A`` is the low-rank weight delta) but avoids materializing the potentially huge ``(out_features, in_features)`` matrix. :param x: Input tensor of shape ``(pop_size, batch, in_features)``. :param A: Factor ``A`` of shape ``(pop_size, rank, in_features)``. :param B: Factor ``B`` of shape ``(pop_size, out_features, rank)``. :param sigma: Scaling factor applied to the delta. :return: Output of shape ``(pop_size, batch, out_features)``. """ # temp: (pop, batch, rank) temp = torch.bmm(x, A.transpose(-1, -2)) # delta: (pop, batch, out_features) delta = sigma * torch.bmm(temp, B.transpose(-1, -2)) return delta
[docs] def lora_gradient( fitness: torch.Tensor, A: torch.Tensor, B: torch.Tensor | None, pop_size: int, sigma: float, weight_shape: tuple, ) -> torch.Tensor: """Estimate the weight gradient from population fitness and LoRA factors. This implements the population-based gradient estimate used in zeroth-order / evolution-strategies style optimization. For each individual ``i`` the perturbation applied to the weight is ``sigma * delta_W_i`` where ``delta_W_i = B_i @ A_i`` (2-D) or the flat noise (1-D). The gradient is the fitness-weighted average of these perturbations, normalized by ``pop_size * sigma``. - **1-D** (``B is None``): ``grad = (fitness / (pop_size * sigma)) @ noise``, reshaped to ``weight_shape``. - **2-D / >2-D**: ``grad = einsum('i,ijk,ikl->jl', fitness, B, A) / (pop_size * sigma)``, reshaped to ``weight_shape``. :param fitness: 1-D tensor of per-individual fitness values ``(pop_size,)``. :param A: For 2-D, factor ``A`` of shape ``(pop_size, rank, k)``; for 1-D, the flat noise of shape ``(pop_size, n)``. :param B: For 2-D, factor ``B`` of shape ``(pop_size, d, rank)``; for 1-D, ``None``. :param pop_size: The population size (number of individuals). :param sigma: The perturbation scaling factor used during evaluation. :param weight_shape: The target weight tensor shape for the output gradient. :return: Gradient tensor reshaped to ``weight_shape``. """ if B is None: # 1-D weight: A is the flat noise (pop_size, n). grad = (fitness / (pop_size * sigma)) @ A return grad.reshape(weight_shape) else: # 2-D weight: grad = sum_i fitness_i * (B_i @ A_i) / (pop_size * sigma) grad = torch.einsum("i,ijk,ikl->jl", fitness, B, A) / (pop_size * sigma) return grad.reshape(weight_shape)