Skip to content

Commit 6842b21

Browse files
author
Hoang Phan
committed
Fix Blelloch exclusive scan: avoid numerical instability
Changed Blelloch scan to compute exclusive prefix directly instead of converting from inclusive, avoiding division by lambda^n which causes overflow when lambda is small. Implementation: 1. Compute inclusive prefix using standard up-sweep + down-sweep 2. Convert to exclusive via simple rank shift: each rank i receives inclusive[i-1] from rank i-1, rank 0 gets zero This matches the pattern used in lasp_naive where the ring naturally produces exclusive prefix, avoiding the numerical issues of computing 1/lambda^n which overflows to infinity when s >= 1.0. Fixes NaN gradients in backward pass.
1 parent c5cd122 commit 6842b21

2 files changed

Lines changed: 36 additions & 56 deletions

File tree

lasp/lasp_blelloch.py

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -161,30 +161,8 @@ def forward(ctx, q, k, v, s, KV, DKV):
161161
)
162162

163163
# Blelloch scan: O(log P) tree communication
164-
# IMPORTANT: Blelloch returns INCLUSIVE prefix (includes current rank)
165-
# but LASP needs EXCLUSIVE prefix (only previous ranks)
166-
KV_prefix_inclusive = scanner.scan(local_kv)
167-
168-
# Convert inclusive to exclusive
169-
# For the LASP associative operation (λ^C, KV), we have:
170-
# inclusive[i] = λ^(C*i)*KV[0] + ... + λ^C*KV[i-1] + KV[i]
171-
# exclusive[i] = λ^(C*(i-1))*KV[0] + ... + KV[i-1]
172-
#
173-
# To convert: exclusive = λ^(-C) * (inclusive - KV[i])
174-
#
175-
# NOTE: Create new tensor instead of modifying KV with .copy_()
176-
# This avoids modifying input buffers which can cause issues
177-
if rank > 0:
178-
# Compute λ^(-C) = 1 / λ^C
179-
lambda_C_inv = 1.0 / lambda_decay ** n
180-
# Expand to match tensor dimensions [h] → [b, h, d, e]
181-
lambda_C_inv_expanded = lambda_C_inv.view(1, h, 1, 1).expand(b, h, d, e)
182-
# exclusive = λ^(-C) * (inclusive - local)
183-
KV_prefix = lambda_C_inv_expanded * (KV_prefix_inclusive - local_kv)
184-
else:
185-
# Rank 0 has no previous ranks, so prefix is zero
186-
# Use KV which is already zeroed
187-
KV_prefix = KV
164+
# Returns EXCLUSIVE prefix (only previous ranks, not including current)
165+
KV_prefix = scanner.scan(local_kv)
188166

189167
# ===== STEP 4: Inter-chunk attention using fused kernel =====
190168
# This is the key improvement: use _fwd_none_diag_kernel instead of torch.matmul
@@ -318,25 +296,8 @@ def backward(ctx, do):
318296
)
319297

320298
# Reverse scan for gradients
321-
# IMPORTANT: Blelloch returns INCLUSIVE suffix (includes current rank)
322-
# but LASP needs EXCLUSIVE suffix (only future ranks)
323-
DKV_suffix_inclusive = scanner.scan(local_dkv)
324-
325-
# Convert inclusive to exclusive
326-
# Same logic as forward: exclusive = λ^(-C) * (inclusive - local)
327-
# NOTE: Create new tensor instead of modifying DKV with .copy_()
328-
# This avoids modifying saved tensors which can cause CUDA errors
329-
if rank < world_size - 1:
330-
# Compute λ^(-C) = 1 / λ^C
331-
lambda_C_inv = 1.0 / lambda_decay ** n
332-
# Expand to match tensor dimensions [h] → [b, h, d, e]
333-
lambda_C_inv_expanded = lambda_C_inv.view(1, h, 1, 1).expand(b, h, d, e)
334-
# exclusive = λ^(-C) * (inclusive - local)
335-
DKV_suffix = lambda_C_inv_expanded * (DKV_suffix_inclusive - local_dkv)
336-
else:
337-
# Last rank (which is rank 0 in forward) has no future ranks
338-
# Return zero suffix (use DKV which is already zeroed)
339-
DKV_suffix = DKV
299+
# Returns EXCLUSIVE suffix (only future ranks, not including current)
300+
DKV_suffix = scanner.scan(local_dkv)
340301

341302
# ===== STEP 4: Inter-chunk gradient contribution using fused kernel =====
342303
with torch.cuda.device(q.device.index):

lasp/utils/blelloch_ops.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -239,22 +239,23 @@ def combine(
239239

240240
def scan(self, local_value: torch.Tensor) -> torch.Tensor:
241241
"""
242-
Perform parallel prefix scan on local KV contribution.
242+
Perform parallel EXCLUSIVE prefix scan on local KV contribution.
243243
244244
Args:
245245
local_value: Local KV state b[rank] (shape: [b, h, d, e])
246246
247247
Returns:
248-
prefix_sum: KV[0:rank+1] - prefix sum up to this rank
248+
exclusive_prefix: KV[0:rank] - prefix sum excluding current rank
249+
(rank 0 gets zero, rank i gets sum from ranks 0 to i-1)
249250
"""
250251
if self.world_size == 1:
251-
# Single GPU: no communication needed
252-
return local_value
252+
# Single GPU: exclusive prefix is zero (no previous ranks)
253+
return torch.zeros_like(local_value)
253254

254255
b, h, d, e = local_value.shape
255256

256257
# ============ UP-SWEEP PHASE ============
257-
# Build tree bottom-up, accumulating partial sums
258+
# Build tree bottom-up, accumulating partial sums (inclusive)
258259

259260
current_value = local_value.clone()
260261
tree_values = [current_value] # Store for down-sweep
@@ -283,9 +284,9 @@ def scan(self, local_value: torch.Tensor) -> torch.Tensor:
283284
tree_values.append(current_value)
284285

285286
# ============ DOWN-SWEEP PHASE ============
286-
# Distribute prefix sums top-down
287+
# Distribute inclusive prefix sums top-down
287288

288-
prefix_sum = None
289+
inclusive_prefix = None
289290

290291
for level in range(self.num_levels - 1, -1, -1):
291292
partner = self.get_partner_rank(level, 'down')
@@ -304,19 +305,37 @@ def scan(self, local_value: torch.Tensor) -> torch.Tensor:
304305
distance = abs(self.scan_rank - partner)
305306
# Use the tree value stored during up-sweep
306307
tree_idx = min(level, len(tree_values) - 1)
307-
prefix_sum = self.combine(left_prefix, tree_values[tree_idx], distance)
308+
inclusive_prefix = self.combine(left_prefix, tree_values[tree_idx], distance)
308309

309310
elif self.is_sender(level, 'down') and partner < self.world_size:
310311
# Send to right child (convert to global rank)
311312
global_partner = self.local_to_global_rank(partner)
312-
send_value = prefix_sum if prefix_sum is not None else tree_values[min(level, len(tree_values) - 1)]
313+
send_value = inclusive_prefix if inclusive_prefix is not None else tree_values[min(level, len(tree_values) - 1)]
313314
dist.send(tensor=send_value.contiguous(), dst=global_partner, group=self.group)
314315

315-
# Rank 0 has no left prefix, uses its accumulated tree value
316-
if prefix_sum is None:
317-
prefix_sum = tree_values[-1] if len(tree_values) > 1 else local_value
316+
# Compute inclusive prefix for this rank
317+
if inclusive_prefix is None:
318+
inclusive_prefix = tree_values[-1] if len(tree_values) > 1 else local_value
318319

319-
return prefix_sum
320+
# ============ CONVERT TO EXCLUSIVE ============
321+
# Simple approach: rank i sends inclusive[i] to rank i+1
322+
# Rank 0 returns zero, rank i returns inclusive[i-1]
323+
324+
exclusive_prefix = torch.zeros_like(local_value)
325+
326+
if self.scan_rank > 0:
327+
# Receive from left neighbor (scan_rank - 1)
328+
left_neighbor = self.scan_rank - 1
329+
global_left = self.local_to_global_rank(left_neighbor)
330+
dist.recv(tensor=exclusive_prefix, src=global_left, group=self.group)
331+
332+
if self.scan_rank < self.world_size - 1:
333+
# Send to right neighbor (scan_rank + 1)
334+
right_neighbor = self.scan_rank + 1
335+
global_right = self.local_to_global_rank(right_neighbor)
336+
dist.send(tensor=inclusive_prefix.contiguous(), dst=global_right, group=self.group)
337+
338+
return exclusive_prefix
320339

321340

322341
def safe_decay_power(base: float, exponent: int, use_log_space: bool = True) -> float:

0 commit comments

Comments
 (0)