Skip to content

Commit df34e43

Browse files
committed
Refactor low-rank forward kernel implementation and enhance external parameter handling
- Removed redundant kernels and consolidated logic in the low-rank forward computation. - Introduced a new `tie_external` option in `LowRankSumParams` to allow sharing of external factors across tied nodes, optimizing memory usage and performance. - Updated the forward and backward methods to accommodate the new parameter tying mechanism. - Added tests to validate the behavior of tied external parameters and ensure gradient accumulation is correctly implemented. - Improved documentation and comments for clarity on kernel behavior and parameter interactions.
1 parent 61dd68c commit df34e43

8 files changed

Lines changed: 273 additions & 488 deletions

File tree

‎src/pyjuice/layer/external_sum_layer.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ def _compile_partition_edge_block_ids(self) -> None:
307307
# shapes at `batch_size = 1` gives sizes in per-batch units directly.
308308
unit_base, ns2unit = 0, dict()
309309
for ns in self.nodes:
310+
# Nodes that share storage share their offsets too, and must not advance the cursor
311+
owner = ns.external_params.storage_owner(ns)
312+
if owner is not ns and owner in ns2unit:
313+
ns2unit[ns] = ns2unit[owner]
314+
continue
315+
310316
slots = []
311317
for shape in ns.external_params.storage_shapes(ns, 1):
312318
slots.append(unit_base)

‎src/pyjuice/model/tensorcircuit.py‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -633,16 +633,30 @@ def _external_params_layout(self, batch_size: int):
633633
if layout is not None:
634634
return layout
635635

636+
# Two passes so that a node may share another node's slots (see
637+
# `ExternalSumParams.storage_owner`): allocate for the owners first, then alias the rest onto
638+
# them. Aliased nodes still appear in the map, so the layers and kernels need no special case --
639+
# they look up their own `ns` and simply find the same memory.
636640
offset = 0
637641
ns2slots = dict()
638642
for ns in self.external_params_nodes:
643+
if ns.external_params.storage_owner(ns) is not ns:
644+
continue
645+
639646
slots = []
640647
for shape in ns.external_params.storage_shapes(ns, batch_size):
641648
slots.append((offset, tuple(shape)))
642649
offset += math.prod(shape)
643650

644651
ns2slots[ns] = slots
645652

653+
for ns in self.external_params_nodes:
654+
owner = ns.external_params.storage_owner(ns)
655+
if owner is not ns:
656+
assert owner in ns2slots, \
657+
f"`storage_owner` of {ns} is not itself an external-parameter node of this circuit."
658+
ns2slots[ns] = ns2slots[owner]
659+
646660
layout = (offset, ns2slots)
647661
self._external_params_layouts[batch_size] = layout
648662

@@ -714,9 +728,16 @@ def _stage_external_params(self, kwargs: dict, batch_size: int) -> None:
714728
torch._foreach_copy_(dsts, srcs)
715729

716730
# Nodes the caller did NOT supply keep whatever the buffer held; drop them so a layer sees
717-
# exactly the set that was staged
731+
# exactly the set that was staged.
732+
#
733+
# Membership is by STORAGE GROUP, not by node: when several nodes share one set of tensors (see
734+
# `ExternalSumParams.storage_owner`) the caller supplies them once, and every node in that group
735+
# is staged. Comparing node-by-node would keep only the one that was named and silently turn the
736+
# rest into plain sum layers -- correct-looking output with the correction applied to one
737+
# timestep out of many.
738+
supplied_groups = {ns.external_params.storage_owner(ns) for ns in ns2tensors}
718739
for ns in list(views.keys()):
719-
if ns not in ns2tensors:
740+
if ns.external_params.storage_owner(ns) not in supplied_groups:
720741
del views[ns]
721742

722743
kwargs[EXTERNAL_PARAMS_KWARG] = views

‎src/pyjuice/nodes/external_params/external_params.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ def get_signature(self) -> str:
4242
"""
4343
raise NotImplementedError()
4444

45+
def storage_owner(self, ns):
46+
"""
47+
The node whose staging slots `ns` uses.
48+
49+
Identity by default: every node gets its own slots. A parameterization may return a DIFFERENT
50+
node to make several nodes share one set of external tensors -- e.g. along a parameter-tying
51+
relation, so one factor pair serves every copy of a tied layer. Sharing changes two things for
52+
the caller: tensors are supplied once, for the owner, and the gradient returned for the owner is
53+
the SUM over every node that shares it.
54+
"""
55+
return ns
56+
4557
def validate_ns(self, ns) -> None:
4658
"""
4759
Check that `ns` is compatible with this parameterization. Called once, at node construction.

‎src/pyjuice/nodes/external_params/kernels/c/lowrank_backward.cu‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ __global__ void lowrank_bw_v_kernel(
4848
const int64_t* __restrict__ nids, const int64_t* __restrict__ xv,
4949
float* __restrict__ grad_ext, float* __restrict__ p_lp, float* __restrict__ p_lq,
5050
int batch_size, int num_eblks, int block_size, int rank,
51-
int n_ntiles, int tile_n, long ext_base, int TB) {
51+
int n_ntiles, int tile_n, long ext_base, int TB, int accumulate) {
5252

5353
const int re = blockIdx.x; // row * num_eblks + edge block
5454
const int nt = blockIdx.y; // node tile
@@ -118,11 +118,21 @@ __global__ void lowrank_bw_v_kernel(
118118
const long vo = (xv_v + (long)n * rank + r) * batch_size + b;
119119
const float v = ext[vo];
120120

121-
// Each (n, r, b) is touched by exactly one thread (nids partition the nodes), so a plain store
122-
// is safe and avoids reading a gradient buffer that is as large as `V` itself. Kept in the
123-
// exponent rather than forming `f` first, so a tiny flow does not lose the factor.
121+
// Kept in the exponent rather than forming `f` first, so a tiny flow does not lose the factor.
124122
const float lfv = lf + v;
125-
grad_ext[vo] = safe_exp_diff(lfv + lw, lt) - safe_exp_diff(lfv + la, lz);
123+
const float g = safe_exp_diff(lfv + lw, lt) - safe_exp_diff(lfv + la, lz);
124+
125+
// A plain STORE is correct when this node owns its factors: `nids` partition the nodes, so each
126+
// (n, r, b) is touched exactly once, and it avoids reading a gradient buffer as large as `V`.
127+
// When several nodes SHARE one factor pair, every one of them contributes to the same gradient,
128+
// so it has to accumulate. Layers run sequentially and each element is still written once per
129+
// layer, so a plain read-modify-write suffices -- no atomics -- and the buffer is zeroed once
130+
// per backward pass, which is the precondition this relies on.
131+
if (accumulate) {
132+
grad_ext[vo] += g;
133+
} else {
134+
grad_ext[vo] = g;
135+
}
126136

127137
lse_add(acc_p[w], safe_log_diff(lfv, lt));
128138
lse_add(acc_q[w], safe_log_diff(lfv, lz));
@@ -181,7 +191,7 @@ __global__ void lowrank_bw_u_kernel(
181191
const int64_t* __restrict__ cids, const int64_t* __restrict__ xu,
182192
float* __restrict__ grad_ext, float* __restrict__ element_flows,
183193
int batch_size, int num_edges, int num_eblks, int ch_block_size, int rank,
184-
int tile_c, long ext_base, int TB) {
194+
int tile_c, long ext_base, int TB, int accumulate) {
185195

186196
const int re = blockIdx.x;
187197
const int ct = blockIdx.y;
@@ -214,8 +224,16 @@ __global__ void lowrank_bw_u_kernel(
214224
const float u = ext[uo];
215225

216226
// The positive term is both dLL/dU's first part and this child's share of the flow
217-
grad_ext[uo] = expf(u + e + lp) - expf(u + lq);
218-
lse_add(log_corr, u + e + lp);
227+
const float log_t1 = u + e + lp; // LOG space -- `log_corr` accumulates logs
228+
const float g = expf(log_t1) - expf(u + lq);
229+
230+
if (accumulate) { // see the note in pass A
231+
grad_ext[uo] += g;
232+
} else {
233+
grad_ext[uo] = g;
234+
}
235+
236+
lse_add(log_corr, log_t1);
219237
}
220238

221239
// Atomic because several parent blocks may share a child block: unlike `node_mars` in the forward,
@@ -266,7 +284,7 @@ void lowrank_backward(torch::Tensor node_flows, torch::Tensor element_flows,
266284
torch::Tensor p_lp, torch::Tensor p_lq,
267285
torch::Tensor log_p, torch::Tensor log_q,
268286
int64_t block_size, int64_t ch_block_size, int64_t rank, int64_t ext_base,
269-
int64_t tile_n, int64_t tile_c, int64_t tb) {
287+
int64_t tile_n, int64_t tile_c, int64_t tb, bool accumulate) {
270288
271289
const int batch_size = node_flows.size(1);
272290
const int rows = xu.size(0);
@@ -285,7 +303,7 @@ void lowrank_backward(torch::Tensor node_flows, torch::Tensor element_flows,
285303
nids.data_ptr<int64_t>(), xv.data_ptr<int64_t>(),
286304
grad_ext.data_ptr<float>(), p_lp.data_ptr<float>(), p_lq.data_ptr<float>(),
287305
batch_size, num_eblks, (int)block_size, (int)rank,
288-
n_ntiles, (int)tile_n, (long)ext_base, (int)tb);
306+
n_ntiles, (int)tile_n, (long)ext_base, (int)tb, accumulate ? 1 : 0);
289307
290308
const int n_outputs = rows * num_eblks * (int)rank * batch_size;
291309
lowrank_bw_pq_reduce_kernel<<<bw_cdiv(n_outputs * 32, 256), 256, 0, stream>>>(
@@ -301,5 +319,5 @@ void lowrank_backward(torch::Tensor node_flows, torch::Tensor element_flows,
301319
cids.data_ptr<int64_t>(), xu.data_ptr<int64_t>(),
302320
grad_ext.data_ptr<float>(), element_flows.data_ptr<float>(),
303321
batch_size, num_edges, num_eblks, (int)ch_block_size, (int)rank,
304-
(int)tile_c, (long)ext_base, (int)tb);
322+
(int)tile_c, (long)ext_base, (int)tb, accumulate ? 1 : 0);
305323
}

‎src/pyjuice/nodes/external_params/kernels/c/lowrank_forward.cu‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ void lowrank_backward(torch::Tensor node_flows, torch::Tensor element_flows,
288288
torch::Tensor p_lp, torch::Tensor p_lq,
289289
torch::Tensor log_p, torch::Tensor log_q,
290290
int64_t block_size, int64_t ch_block_size, int64_t rank, int64_t ext_base,
291-
int64_t tile_n, int64_t tile_c, int64_t tb);
291+
int64_t tile_n, int64_t tile_c, int64_t tb, bool accumulate);
292292

293293
void lowrank_shift_logz(torch::Tensor node_mars, torch::Tensor nids, torch::Tensor log_z,
294294
int64_t block_size, double sign);

0 commit comments

Comments
 (0)