Source code for brainevent._jit_normal.dt2t

# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

# -*- coding: utf-8 -*-

"""
Direct per-synapse ``y * w`` generation for normal-weight just-in-time
connectivity (JITC) matrices, on the light-RNG (mv) kernels.

:func:`jitnmv_dt2t` returns one value per generated structural non-zero in
canonical CSR flat order (the same order as ``jitn_to_csr``),
namely ``sampled_weight * y[row]`` (``transpose=False``) or ``sampled_weight * y[col]``
(``transpose=True``).  It always materializes the mv matrix.

``corder`` keeps its usual meaning (it selects the notrans/trans generation).  The
``corder=True`` path can use the fused fill primitive (which replays the mv-notrans
walk); the ``corder=False`` path composes over :func:`jitn_to_csr` (whose trans
materialization has a different flat order the fused kernel can't reproduce).
"""

from pathlib import Path
from typing import Optional

import brainunit as u
import jax
import jax.numpy as jnp
import numpy as np

from brainevent._compatible_import import Tracer
from brainevent._data import _initialize_seed
from brainevent._numba_random import get_numba_light_rng_funcs
from brainevent._op import XLACustomKernel, load_cuda_file, numba_kernel
from brainevent._typing import MatrixShape
from .csr import jitn_to_csr
from .float import _LANE_STRIDE, _chunk_size, _walk_length
from brainevent._op.util import dtype_suffix

__all__ = [
    'jitnmv_dt2t',
    'jitnmv_dt2t_p',
    'jitnmv_dt2t_p_call',
]


[docs] def jitnmv_dt2t( w_loc, w_scale, prob, y, seed, *, shape: MatrixShape, transpose: bool = False, corder: bool = True, backend: Optional[str] = None, ): """Generate per-synapse ``y * w`` values for a normal JITC (mv) matrix.""" shape = (int(shape[0]), int(shape[1])) n_rows, n_cols = shape u.fail_for_dimension_mismatch(w_loc, w_scale, "w_loc and w_scale must have the same dimension.") w_loc, unitd = u.split_mantissa_unit(w_loc) w_scale = u.Quantity(w_scale).to(unitd).mantissa y, unity = u.split_mantissa_unit(y) common_dtype = jnp.result_type(w_loc, w_scale, y) w_loc = jnp.atleast_1d(jnp.asarray(w_loc, dtype=common_dtype)) w_scale = jnp.atleast_1d(jnp.asarray(w_scale, dtype=common_dtype)) y = jnp.asarray(y, dtype=common_dtype) seed = _initialize_seed(seed) if y.ndim != 1: raise AssertionError("y must be 1D.") if transpose: assert n_cols == y.shape[0], "Shape mismatch for transpose operation." else: assert n_rows == y.shape[0], "Shape mismatch for non-transpose operation." if not isinstance(prob, Tracer) and float(np.asarray(prob)) == 0.0: data = jnp.zeros(0, dtype=common_dtype) return u.maybe_decimal(data * unitd * unity) # Materialize the canonical (column-sorted) mv CSR; dt2t is ``weight * y`` at # each structural non-zero, taken in that CSR's flat order. Composing over # ``jitn_to_csr`` keeps both corder values and both directions consistent with # the materialized structure and is deterministic. csr = jitn_to_csr( w_loc, w_scale, prob, seed, shape=shape, corder=corder, backend=backend, ) indptr = csr.indptr nnz = int(indptr[-1]) if nnz == 0: return u.maybe_decimal(jnp.zeros(0, dtype=common_dtype) * unitd * unity) if transpose: gathered = y[csr.indices] # weight * y[col] else: row_ids = jnp.repeat( jnp.arange(n_rows, dtype=jnp.int32), jnp.diff(indptr), total_repeat_length=nnz ) gathered = y[row_ids] # weight * y[row] return u.maybe_decimal(csr.data * gathered * unitd * unity)
# ---------------------------------------------------------------------- # # Fused fill primitive (corder=True / notrans structure), CUDA + numba # ---------------------------------------------------------------------- # def _jitnmv_dt2t_fill_cuda_kernel( shape: MatrixShape, transpose: bool, **kwargs, ): load_cuda_file(Path(__file__).parent.joinpath('dt2t.cu'), name='jit_normal_dt2t') sfx = dtype_suffix(kwargs['w_loc_info'].dtype) direction = 'trans' if transpose else 'notrans' kernel_name = f'jit_normal_dt2t.fill_{direction}{sfx}' n_cols = int(shape[1]) chunk_size_value = _chunk_size(_walk_length(shape, False, True)) def kernel(w_loc, w_scale, clen, y, seed, chunk_offsets): return jax.ffi.ffi_call(kernel_name, kwargs['outs'])( w_loc, w_scale, clen, y, seed, chunk_offsets, n_cols=np.int32(n_cols), chunk_size=np.int32(chunk_size_value), ) return kernel def _jitnmv_dt2t_fill_numba_kernel( shape: MatrixShape, transpose: bool, **kwargs, ): """Numba CPU fused ``dt2t`` fill mirroring ``dt2t.cu``.""" import numba _rng = get_numba_light_rng_funcs() _rng_init = _rng['init'] _rng_next = _rng['next'] _rng_bounded = _rng['bounded'] _rng_initial_q = _rng['initial_q'] _rng_normal01 = _rng['normal01'] stride = _LANE_STRIDE k = int(shape[1]) cs_val = _chunk_size(_walk_length(shape, False, True)) if transpose: @numba.njit(fastmath=True) def kernel_impl(w_loc, w_scale, clen, y, seed, chunk_offsets, data): m = chunk_offsets.shape[0] n_chunks = chunk_offsets.shape[1] w_loc0 = w_loc[0] w_scale0 = w_scale[0] seed0 = np.uint32(seed[0]) cl = np.uint32(clen[0]) if cl < np.uint32(2): cl = np.uint32(2) for row in range(m): for chunk_id in range(n_chunks): chunk_start = chunk_id * cs_val if chunk_start >= k: continue chunk_end = chunk_start + cs_val if chunk_end > k: chunk_end = k chunk_width = chunk_end - chunk_start pos = chunk_offsets[row, chunk_id] for lane in range(stride): state = _rng_init(seed0, row, chunk_id, lane) q, state = _rng_initial_q(state, cl) local_j = lane + stride * int(q) while local_j < chunk_width: j = chunk_start + local_j n01 = _rng_normal01(seed0, row, j) data[pos] = (w_loc0 + n01 * w_scale0) * y[j] pos += 1 state = _rng_next(state) q = q + np.uint32(1) + _rng_bounded(state, cl - np.uint32(1)) local_j = lane + stride * int(q) else: @numba.njit(fastmath=True) def kernel_impl(w_loc, w_scale, clen, y, seed, chunk_offsets, data): m = chunk_offsets.shape[0] n_chunks = chunk_offsets.shape[1] w_loc0 = w_loc[0] w_scale0 = w_scale[0] seed0 = np.uint32(seed[0]) cl = np.uint32(clen[0]) if cl < np.uint32(2): cl = np.uint32(2) for row in range(m): yrow = y[row] for chunk_id in range(n_chunks): chunk_start = chunk_id * cs_val if chunk_start >= k: continue chunk_end = chunk_start + cs_val if chunk_end > k: chunk_end = k chunk_width = chunk_end - chunk_start pos = chunk_offsets[row, chunk_id] for lane in range(stride): state = _rng_init(seed0, row, chunk_id, lane) q, state = _rng_initial_q(state, cl) local_j = lane + stride * int(q) while local_j < chunk_width: j = chunk_start + local_j n01 = _rng_normal01(seed0, row, j) data[pos] = (w_loc0 + n01 * w_scale0) * yrow pos += 1 state = _rng_next(state) q = q + np.uint32(1) + _rng_bounded(state, cl - np.uint32(1)) local_j = lane + stride * int(q) def kernel(w_loc, w_scale, clen, y, seed, chunk_offsets): return numba_kernel(kernel_impl, outs=kwargs['outs'])(w_loc, w_scale, clen, y, seed, chunk_offsets) return kernel def jitnmv_dt2t_p_call( w_loc, w_scale, clen, y, seed, chunk_offsets, nnz: int, *, shape: MatrixShape, transpose: bool = False, backend: Optional[str] = None, ): """Fused ``dt2t`` fill over the mv-notrans structure (``chunk_offsets`` is the per-(row, chunk) exclusive-offset table). Returns ``(data,)`` of length ``nnz``.""" w_loc = jnp.atleast_1d(w_loc) w_scale = jnp.atleast_1d(w_scale) clen = jnp.atleast_1d(clen) y = jnp.asarray(y) seed = jnp.atleast_1d(seed) chunk_offsets = jnp.asarray(chunk_offsets, dtype=jnp.int32) assert len(shape) == 2, f"shape must be two-dimensional, but got {shape}." assert y.ndim == 1, "y must be 1D." assert chunk_offsets.ndim == 2, "chunk_offsets must be 2D (n_rows, n_chunks)." assert jnp.issubdtype(w_loc.dtype, jnp.floating), "w_loc must be a floating-point type." assert jnp.issubdtype(w_scale.dtype, jnp.floating), "w_scale must be a floating-point type." assert w_loc.dtype == w_scale.dtype == y.dtype, ( f"w_loc, w_scale, and y must share dtype, got {w_loc.dtype}, {w_scale.dtype}, {y.dtype}." ) if transpose: assert shape[1] == y.shape[0], "Shape mismatch for transpose operation." else: assert shape[0] == y.shape[0], "Shape mismatch for non-transpose operation." return jitnmv_dt2t_p( w_loc, w_scale, clen, y, seed, chunk_offsets, outs=[jax.ShapeDtypeStruct((int(nnz),), y.dtype)], shape=(int(shape[0]), int(shape[1])), transpose=transpose, backend=backend, w_loc_info=jax.ShapeDtypeStruct(w_loc.shape, w_loc.dtype), w_scale_info=jax.ShapeDtypeStruct(w_scale.shape, w_scale.dtype), clen_info=jax.ShapeDtypeStruct(clen.shape, clen.dtype), y_info=jax.ShapeDtypeStruct(y.shape, y.dtype), seed_info=jax.ShapeDtypeStruct(seed.shape, seed.dtype), chunk_offsets_info=jax.ShapeDtypeStruct(chunk_offsets.shape, chunk_offsets.dtype), ) jitnmv_dt2t_p = XLACustomKernel( 'jitnmv_dt2t_fill', doc=""" Low-level CUDA primitive filling per-synapse ``sampled_weight * y`` values for a normal JITC (mv) matrix, in the mv-notrans CSR flat order. """, ) jitnmv_dt2t_p.def_cuda_raw_kernel(_jitnmv_dt2t_fill_cuda_kernel, asdefault=True) jitnmv_dt2t_p.def_numba_kernel(_jitnmv_dt2t_fill_numba_kernel) jitnmv_dt2t_p.def_call(jitnmv_dt2t_p_call) jitnmv_dt2t_p.def_tags('jit_normal', 'dt2t', 'light_rng')