Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
edb23d0
define external sum nodes
liuanji Jul 29, 2026
a809d80
io for external sum nodes
liuanji Jul 29, 2026
9fe5c66
adapt transformations for external sum nodes
liuanji Jul 29, 2026
7bf8da3
runtests for external sum node definition
liuanji Jul 29, 2026
84c160b
fix bug in `merge_by_region_node`
liuanji Jul 29, 2026
2f80cf6
skeleton for external parameter sum layer
liuanji Jul 29, 2026
f41937d
runtests for external sum layers
liuanji Jul 29, 2026
d346ea5
fix `add_missing_flows` bug in PCs with tied input nodes
liuanji Jul 29, 2026
4df8445
API overall build for external param sum layers
liuanji Jul 29, 2026
94f211f
runtests for external params sum layer main APIs
liuanji Jul 29, 2026
bf4fea4
hmm w/ external params check
liuanji Jul 29, 2026
ee8e091
forward kernel for lowrank ext sum layer
liuanji Jul 30, 2026
fee5c7e
runtests for lowrank forward sum layer
liuanji Jul 30, 2026
0406221
lowrank ext sum fw/bk implementation and speedup
liuanji Jul 30, 2026
df43928
make `HCLT` support ext sum nodes
liuanji Jul 30, 2026
61dd68c
runtests for ext sum layer fw/bk
liuanji Jul 30, 2026
df34e43
Refactor low-rank forward kernel implementation and enhance external …
liuanji Jul 30, 2026
a549ee5
Fix external parameter handling for shared owners across layers and a…
liuanji Jul 30, 2026
e419ec0
ext lowrank sum training test
liuanji Jul 30, 2026
92bfe71
extern lowrank hmm test
liuanji Jul 30, 2026
73e69f3
Refactor external parameter handling in LowRankSumParams to use ext_s…
liuanji Jul 30, 2026
a36a842
Define `BlockScaleSumParams`
liuanji Jul 30, 2026
a4bf539
Remove lowrank_backward and lowrank_shift_logz function definitions f…
liuanji Jul 30, 2026
f7731d8
a separate binding file for kernel declarations
liuanji Jul 30, 2026
eaf78b9
forward kernel for blockscale
liuanji Jul 30, 2026
13d66d6
halfway done with blockscale forward speed optimization
liuanji Jul 30, 2026
cadbe3e
further speedup blockscale forward
liuanji Jul 30, 2026
7f7ee06
Enhance blockscale forward kernel with shared memory calculations and…
liuanji Jul 30, 2026
3dbda00
Add CUDA exception handling and kernel launch checks across multiple …
liuanji Jul 31, 2026
64f088c
build and improve small batch kernels for external blockscale
liuanji Jul 31, 2026
a86c485
Refactor tensor handling in TensorCircuit to improve layout managemen…
liuanji Jul 31, 2026
49f4e38
API change for external blockscale
liuanji Jul 31, 2026
8ca389d
Enhance external sum layer and block scale parameters with storage of…
liuanji Jul 31, 2026
070877b
Implement staging transpose for batch-innermost external parameters a…
liuanji Jul 31, 2026
089ae97
Refactor blockscale_forward kernel: remove unused sigma parameter and…
liuanji Jul 31, 2026
59ca213
blockscale_par_backward kernel
liuanji Jul 31, 2026
72b39a4
wiring of block scale layer kernels
liuanji Jul 31, 2026
73a18c5
Add tests for backward propagation with two gated layers and plain la…
liuanji Jul 31, 2026
463bea1
reorganize test files
liuanji Jul 31, 2026
265c4a6
speedup blockscale bk kernels
liuanji Jul 31, 2026
ce2a9d2
Refactor external parameter hooks and add Triton kernel for BlockScal…
liuanji Jul 31, 2026
f46b4a7
Add comprehensive tests for BlockScaleSumParams across various shapes…
liuanji Jul 31, 2026
3994a72
Implement external parameters group registration and associated tests
liuanji Jul 31, 2026
b45c35b
Add end-to-end test for HMM with per-sample gates using BlockScaleSum…
liuanji Jul 31, 2026
6d8f7b4
Add tests for EM monotonicity and pseudocount behavior with live gates
liuanji Jul 31, 2026
4224106
Add Triton support for parameter flows in BlockScaleSumParams and rel…
liuanji Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pyjuice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pyjuice.model import compile, TensorCircuit

# TensorCircuit layers
from pyjuice.layer import InputLayer, ProdLayer, SumLayer
from pyjuice.layer import InputLayer, ProdLayer, SumLayer, ExternalParamsSumLayer

# Construction methods
from pyjuice.nodes import multiply, summate, inputs, set_block_size, structural_properties
Expand Down
12 changes: 10 additions & 2 deletions src/pyjuice/io/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from functools import partial
from typing import Sequence

from pyjuice.nodes import CircuitNodes, InputNodes, ProdNodes, SumNodes, inputs, multiply, summate
from pyjuice.nodes import CircuitNodes, InputNodes, ProdNodes, SumNodes, ExternalParamsSumNodes, inputs, multiply, summate


def serialize_nodes(root_ns: CircuitNodes):
Expand All @@ -30,6 +30,9 @@ def serialize_nodes(root_ns: CircuitNodes):
if ns.is_prod() or ns.is_sum():
ns_info["edge_ids"] = ns.edge_ids.detach().cpu().numpy().copy()

if isinstance(ns, ExternalParamsSumNodes):
ns_info["external_params"] = pickle.dumps(ns.external_params)

if hasattr(ns, "_params") and ns._params is not None:
ns_info["params"] = ns._params.detach().cpu().numpy().copy()

Expand Down Expand Up @@ -102,7 +105,12 @@ def deserialize_nodes(nodes_list: Sequence):
else:
params = None

ns = summate(*chs, edge_ids = edge_ids, params = params, block_size = block_size)
external_params = ns_info.get("external_params", None)
if external_params is not None:
external_params = pickle.loads(external_params)

ns = summate(*chs, edge_ids = edge_ids, params = params, block_size = block_size,
external_params = external_params)

if "zero_param_mask" in ns_info:
zero_param_mask = torch.from_numpy(ns_info["zero_param_mask"])
Expand Down
3 changes: 3 additions & 0 deletions src/pyjuice/layer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
from .input_layer import InputLayer
from .prod_layer import ProdLayer
from .sum_layer import SumLayer
from .external_sum_layer import ExternalParamsSumLayer, ExternalNodeInfo, StagedExternalParams, \
EXTERNAL_PARAMS_BUFFER_KWARG, EXTERNAL_PARAMS_GRAD_BUFFER_KWARG, \
EXTERNAL_PARAMS_KWARG, EXTERNAL_PARAMS_GRAD_KWARG
from .layer_group import LayerGroup
613 changes: 613 additions & 0 deletions src/pyjuice/layer/external_sum_layer.py

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions src/pyjuice/layer/input_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,11 @@ def add_missing_flows(self, node_flows: torch.Tensor, node_mars: Optional[torch.
TILE_SIZE_K = 1

if _pre_accum_nflows:
# This launch walks the SOURCE nodes -- it indexes `source_nids`, which has one entry
# per source node, not per node -- so its bound is `layer_num_source_nodes`. Bounding
# it by `layer_num_nodes` instead reads `source_nids` past its end whenever the layer
# has tied nodes, and the garbage node ids it returns are then used to gather
# `node_flows` / `s_pids` / `s_pfids` and to scatter into `param_flows`.
layer_num_source_nodes = self.source_nids.size(0)

self._accum_flows_to_tied_nodes(node_flows, logspace_flows)
Expand All @@ -871,7 +876,7 @@ def add_missing_flows(self, node_flows: torch.Tensor, node_mars: Optional[torch.
source_nids_ptr = self.source_nids,
scale = scale,
node_offset = node_offset,
layer_num_nodes = layer_num_nodes,
num_active_nodes = layer_num_source_nodes,
logspace_flows = logspace_flows,
are_source_nodes = True,
BLOCK_SIZE = BLOCK_SIZE,
Expand All @@ -892,7 +897,7 @@ def add_missing_flows(self, node_flows: torch.Tensor, node_mars: Optional[torch.
source_nids_ptr = self.source_nids,
scale = scale,
node_offset = node_offset,
layer_num_nodes = layer_num_nodes,
num_active_nodes = layer_num_nodes,
logspace_flows = logspace_flows,
are_source_nodes = False,
BLOCK_SIZE = BLOCK_SIZE,
Expand Down Expand Up @@ -1221,13 +1226,13 @@ def _flows_kernel_template(flow_fn, params_ptr, param_flows_ptr, node_flows_ptr,

@staticmethod
def _missing_flows_kernel_template(node_flows_ptr, params_ptr, param_flows_ptr, s_pids_ptr, s_pfids_ptr,
metadata_ptr, s_mids_ptr, source_nids_ptr, scale, node_offset: tl.constexpr, layer_num_nodes: tl.constexpr,
metadata_ptr, s_mids_ptr, source_nids_ptr, scale, node_offset: tl.constexpr, num_active_nodes: tl.constexpr,
logspace_flows: tl.constexpr, are_source_nodes: tl.constexpr, BLOCK_SIZE: tl.constexpr, TILE_SIZE_K: tl.constexpr):
pid = tl.program_id(axis = 0)
block_start = pid * BLOCK_SIZE

offsets = block_start + tl.arange(0, BLOCK_SIZE)
mask = offsets < layer_num_nodes
mask = offsets < num_active_nodes

# Get the local node ids
if are_source_nodes:
Expand Down
2 changes: 2 additions & 0 deletions src/pyjuice/layer/kernels/c/ele_backward_sum.cu
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <cuda_fp16.h>
#include <cuda.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <cute/tensor.hpp>
#include <cute/atom/mma_atom.hpp>
#include <cute/atom/copy_atom.hpp>
Expand Down Expand Up @@ -200,6 +201,7 @@ void ele_backward_sum(torch::Tensor element_flows, torch::Tensor element_mars, t
element_flows.data_ptr<float>(), element_mars.data_ptr<float>(), params.data_ptr<float>(),
chids.data_ptr<long>(), ebase.data_ptr<long>(), pbase.data_ptr<long>(),
(int)batch, (int)BSK, (int)BSM, (int)knt, off, g_descNf, g_descNm);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}
}

Expand Down
17 changes: 12 additions & 5 deletions src/pyjuice/layer/kernels/c/input_layers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ def _jit_plain(name: str, source_file: str):
if not ENABLE_CUDA_KERNELS or not torch.cuda.is_available():
return None
cc = torch.cuda.get_device_capability()
cuda_cflags = ["-O3", f"-arch=sm_{cc[0]}{cc[1]}a", "--use_fast_math", "-DNDEBUG"]
# NO `a` suffix. These kernels use nothing arch-specific, and the suffix only exists for the
# accelerated targets (90a, 100a, 120a, ...) -- there is no `sm_80a` or `sm_86a`, so asking for one
# fails the compile on every pre-Hopper GPU and silently drops the layer onto the Triton path.
cuda_cflags = ["-O3", f"-arch=sm_{cc[0]}{cc[1]}", "--use_fast_math", "-DNDEBUG"]
from pyjuice.utils.cuda_ext import jit_load
try:
return jit_load(name, sources=[os.path.join(_THIS_DIR, source_file)],
Expand All @@ -48,10 +51,14 @@ def cat_backward_is_available() -> bool:

def cat_backward(param_flows: torch.Tensor, node_flows: torch.Tensor, data: torch.Tensor,
vids: torch.Tensor, s_pfids: torch.Tensor, layer_num_nodes: int, batch_size: int,
node_offset: int, num_cats: int, logspace: bool) -> None:
node_offset: int, num_cats: int, logspace: bool) -> bool:
"""Categorical input-layer backward: param_flows[node, cat] += sum_b (exp if logspace) node_flows
where data[vid[node], b] == cat. The flush is NON-ATOMIC, so the caller must guarantee distinct
(untied), 4-aligned ``s_pfids`` and num_cats/batch divisible by 4 (see the gate in
``input_layer.backward``)."""
_cat_module.cat_backward(param_flows, node_flows, data, vids, s_pfids, int(layer_num_nodes),
int(batch_size), int(node_offset), int(num_cats), int(logspace))
``input_layer.backward``).

Returns False WITHOUT running if the ``num_cats``-float histogram exceeds the shared memory this
device grants a block; the caller must then use the Triton path."""
return bool(_cat_module.cat_backward(param_flows, node_flows, data, vids, s_pfids,
int(layer_num_nodes), int(batch_size), int(node_offset),
int(num_cats), int(logspace)))
40 changes: 36 additions & 4 deletions src/pyjuice/layer/kernels/c/input_layers/cat_backward.cu
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// bases (s_pfids multiples of 4, node_offset & batch_size multiples of 4) — enforced by the input_layer gate.
#include <torch/extension.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <cuda_runtime.h>

#ifndef NTH
Expand Down Expand Up @@ -51,17 +52,43 @@ __global__ void cat_backward_kernel(float* __restrict__ param_flows, const float
}
}

void cat_backward(torch::Tensor param_flows, torch::Tensor node_flows, torch::Tensor data,
// The histogram is `num_cats` floats, which for a large vocabulary is well past the 48 KB a block gets
// by default. That ceiling is not a fixed property of the machine -- a block can opt in to most of the
// SM's shared memory, and how much that is varies by part (64 KB, 100 KB, 227 KB) -- so it is queried,
// and the opt-in is requested, rather than either assumed or given up on.
//
// Returns false when even the opt-in ceiling is too small; the caller then falls back to Triton. It is
// a return value and not a `TORCH_CHECK` because this is an optional fast path, and a silent one before:
// the launch was issued unopted-in and unchecked, so past ~12288 categories it failed, `param_flows`
// was never accumulated, and the caller reported success anyway.
bool cat_backward_fits(int num_cats) {
int dev = 0, smax = 0;
cudaGetDevice(&dev);
cudaDeviceGetAttribute(&smax, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev);
return (size_t)num_cats * sizeof(float) <= (size_t)smax;
}


bool cat_backward(torch::Tensor param_flows, torch::Tensor node_flows, torch::Tensor data,
torch::Tensor vids, torch::Tensor s_pfids, int layer_num_nodes, int batch_size,
long node_offset, int num_cats, int logspace) {
int smem = num_cats * sizeof(float);
if (!cat_backward_fits(num_cats)) return false;
auto stream = c10::cuda::getCurrentCUDAStream();
float* pf = param_flows.data_ptr<float>();
const float* nf = node_flows.data_ptr<float>();
const long* vp = vids.data_ptr<long>();
const long* sp = s_pfids.data_ptr<long>();
#define LAUNCH(T) cat_backward_kernel<T><<<layer_num_nodes, NTH, smem, stream>>>( \
pf, nf, data.data_ptr<T>(), vp, sp, layer_num_nodes, batch_size, node_offset, num_cats, logspace)
#define LAUNCH(T) \
do { \
if (smem > 48 * 1024) \
TORCH_CHECK(cudaFuncSetAttribute(cat_backward_kernel<T>, \
cudaFuncAttributeMaxDynamicSharedMemorySize, smem) == cudaSuccess, \
"cat_backward: device refused ", smem, " B of shared memory"); \
cat_backward_kernel<T><<<layer_num_nodes, NTH, smem, stream>>>( \
pf, nf, data.data_ptr<T>(), vp, sp, layer_num_nodes, batch_size, node_offset, num_cats, \
logspace); \
} while (0)
switch (data.scalar_type()) {
case torch::kByte: LAUNCH(uint8_t); break;
case torch::kChar: LAUNCH(int8_t); break;
Expand All @@ -70,9 +97,14 @@ void cat_backward(torch::Tensor param_flows, torch::Tensor node_flows, torch::Te
case torch::kLong: LAUNCH(int64_t); break;
default: TORCH_CHECK(false, "cat_backward: unsupported data dtype ", data.scalar_type());
}
C10_CUDA_KERNEL_LAUNCH_CHECK();
#undef LAUNCH
return true;
}

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("cat_backward", &cat_backward, "Categorical input-layer backward (smem-histogram)");
m.def("cat_backward", &cat_backward,
"Categorical input-layer backward (smem-histogram); false if the histogram does not fit");
m.def("cat_backward_fits", &cat_backward_fits,
"Whether this device grants enough shared memory for a num_cats histogram");
}
4 changes: 3 additions & 1 deletion src/pyjuice/layer/kernels/c/par_backward_sum.cu
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include <cuda_fp16.h>
#include <cuda.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <cute/tensor.hpp>
#include <cute/atom/mma_atom.hpp>
#include <cute/atom/copy_atom.hpp>
Expand Down Expand Up @@ -242,7 +243,8 @@ void par_backward_sum(torch::Tensor param_flows, torch::Tensor node_flows, torch
dim3 grid(gx, chunk);
par_kernel<<<grid, NTH, smem, c10::cuda::getCurrentCUDAStream()>>>(param_flows.data_ptr<float>(), params.data_ptr<float>(),
nbase.data_ptr<long>(), cbase.data_ptr<long>(), pbase.data_ptr<long>(), fbase.data_ptr<long>(),
(int)batch, (int)block_size, (int)num_edges, bnt, (int)use_atomic, off, g_dNf, g_dNm, g_dEm); }
(int)batch, (int)block_size, (int)num_edges, bnt, (int)use_atomic, off, g_dNf, g_dNm, g_dEm);
C10_CUDA_KERNEL_LAUNCH_CHECK(); }
}

PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
Expand Down
2 changes: 2 additions & 0 deletions src/pyjuice/layer/kernels/c/smallbatch_ele_backward.cu
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <vector>

// Combine two online-logsumexp accumulators (max `m`, linear-sum `l`) with a second point/accumulator
Expand Down Expand Up @@ -105,6 +106,7 @@ static void launch_ele(torch::Tensor element_flows, torch::Tensor element_mars,
element_flows.data_ptr<float>(), element_mars.data_ptr<float>(), node_flows.data_ptr<float>(),
node_mars.data_ptr<float>(), params.data_ptr<float>(), chids.data_ptr<long>(),
ebase.data_ptr<long>(), pbase.data_ptr<long>(), batch, block_size, cs_block_size, num_edges);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}

// Dispatch on (WARPS from `cfg`) x (BMAX = next pow2 of batch, for register-tight state).
Expand Down
2 changes: 2 additions & 0 deletions src/pyjuice/layer/kernels/c/smallbatch_forward_sum.cu
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <vector>

template <int SPLIT>
Expand Down Expand Up @@ -81,6 +82,7 @@ static void launch_sb(torch::Tensor node_mars, torch::Tensor element_mars, torch
node_mars.data_ptr<float>(), element_mars.data_ptr<float>(), params.data_ptr<float>(),
nids.data_ptr<long>(), ebase.data_ptr<long>(), pbase.data_ptr<long>(),
batch, block_size, num_edges);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}

// cfg id -> SPLIT (edge-warps). SPLIT=32 was the sweep winner; the autotuner picks per layer/batch.
Expand Down
2 changes: 2 additions & 0 deletions src/pyjuice/layer/kernels/c/smallbatch_par_backward.cu
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <vector>

template <int EY, int BMAX>
Expand Down Expand Up @@ -80,6 +81,7 @@ static void launch_par(torch::Tensor param_flows, torch::Tensor node_flows, torc
param_flows.data_ptr<float>(), node_flows.data_ptr<float>(), node_mars.data_ptr<float>(),
element_mars.data_ptr<float>(), params.data_ptr<float>(), nids.data_ptr<long>(),
cids.data_ptr<long>(), pids.data_ptr<long>(), pfids.data_ptr<long>(), batch, block_size, num_edges);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}

// Dispatch on (EY from `cfg`) x (BMAX = next pow2 of batch, for register-tight state).
Expand Down
2 changes: 2 additions & 0 deletions src/pyjuice/layer/kernels/c/tlmm_forward_sum.cu
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <cuda_bf16.h>
#include <cuda.h>
#include <c10/cuda/CUDAStream.h>
#include <c10/cuda/CUDAException.h>
#include <vector>
#include <cute/tensor.hpp>
#include <cute/atom/mma_atom.hpp>
Expand Down Expand Up @@ -228,6 +229,7 @@ static void launch_cfg(torch::Tensor node_mars, torch::Tensor element_mars, torc
tlmm_kernel<BM, BN, WM, WN><<<grid, NTH, smem, c10::cuda::getCurrentCUDAStream()>>>(
node_mars.data_ptr<float>(), params.data_ptr<float>(), nids.data_ptr<long>(),
ebase.data_ptr<long>(), pbase.data_ptr<long>(), batch, block_size, knt, off, desc);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/pyjuice/layer/layer_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ def __init__(self, layers: Sequence[Layer]):

assert len(layers) >= 1, "A `LayerGroup` must contains at least 1 layer."

# Layers of one group are evaluated interchangeably, so they must be of one kind -- but sum
# layers may legitimately be a mix of `SumLayer` and its external-parameter subclasses, which
# share its forward/backward interface and land at the same depth.
for i in range(1, len(layers)):
assert type(layers[i]) == type(layers[0])
assert type(layers[i]) == type(layers[0]) or \
(isinstance(layers[i], SumLayer) and isinstance(layers[0], SumLayer)), \
f"A `LayerGroup` must contain layers of one kind, got {type(layers[0])} and {type(layers[i])}."

if isinstance(layers[0], InputLayer):
self.layer_type = "input"
Expand Down
Loading
Loading