Scott/Codex commited on
Commit
468a571
·
1 Parent(s): 3c513bc

Add sublinear attention v2 improvements

Browse files
README.md CHANGED
@@ -83,6 +83,6 @@ and a tighter structured-sublinear attention profile (`window=128`, `stride=128`
83
  an ETA around 326 days, under the 1y+90d target, while keeping ctx=1280, B=2,
84
  DiffusionBlocks, gradient-checkpointed blocks, tied heads, and structured masks.
85
 
86
- Sublinear coverage update 2026-05-29: the saved AGILLM-4 trainer snapshot now matches the live run's improved sparse global memory. When the strided anchor list is larger than `--sublinear_max_anchors`, it keeps anchors evenly spread across the whole sequence instead of only the most recent anchors, and it always includes a small set of first-token attention sinks. At the live 128/128/128 profile this preserves full-span coverage for 32k-style contexts instead of going blind to the deep past after the 16k anchor horizon, with essentially the same key budget. See `sublinear_improved_snippet.py` for the minimal drop-in block and `sublinear_improved.py` for the coverage demo/standalone selector.
87
 
88
  License: Apache-2.0 (matching the upstream method).
 
83
  an ETA around 326 days, under the 1y+90d target, while keeping ctx=1280, B=2,
84
  DiffusionBlocks, gradient-checkpointed blocks, tied heads, and structured masks.
85
 
86
+ Sublinear coverage update 2026-05-29: the saved AGILLM-4 trainer snapshot now matches the live v2 sparse global memory path. It fixes gathered ALiBi distance, suppresses duplicate local/anchor candidates before softmax, uses hybrid full-span + recent-tail anchors with explicit `--sublinear_sinks` and `--sublinear_recent_anchors`, and includes optional pooled K/V landmark summaries behind `--sublinear_pooled_landmarks`. At the live 128/128/128 profile it keeps deep-past coverage while preserving recent anchors and the same VRAM-first key budget. See `sublinear_improved_snippet.py` for the minimal blocks and `sublinear_improved.py` for the coverage demo/standalone selector.
87
 
88
  License: Apache-2.0 (matching the upstream method).
nB300_agillm4_vram_dblock.py CHANGED
@@ -926,6 +926,9 @@ DEFAULT_SUBLINEAR_WINDOW = _env_int("AGILLM_SUBLINEAR_WINDOW", 256)
926
  DEFAULT_SUBLINEAR_STRIDE = _env_int("AGILLM_SUBLINEAR_STRIDE", 64)
927
  DEFAULT_SUBLINEAR_MAX_ANCHORS = _env_int("AGILLM_SUBLINEAR_MAX_ANCHORS", 256)
928
  DEFAULT_SUBLINEAR_CHUNK = _env_int("AGILLM_SUBLINEAR_CHUNK", 128)
 
 
 
929
  DEFAULT_ANCHOR_MEMORY = bool(_env_int("AGILLM_ANCHOR_MEMORY", 0))
930
  DEFAULT_ANCHOR_STRIDE = _env_int("AGILLM_ANCHOR_STRIDE", 256)
931
  DEFAULT_ANCHOR_MAX = _env_int("AGILLM_ANCHOR_MAX", 2048)
@@ -1289,6 +1292,9 @@ class TuneableAttentionMHA(nn.Module):
1289
  sublinear_stride: int = DEFAULT_SUBLINEAR_STRIDE,
1290
  sublinear_max_anchors: int = DEFAULT_SUBLINEAR_MAX_ANCHORS,
1291
  sublinear_chunk: int = DEFAULT_SUBLINEAR_CHUNK,
 
 
 
1292
  ):
1293
  super().__init__()
1294
  assert d % h == 0
@@ -1299,6 +1305,12 @@ class TuneableAttentionMHA(nn.Module):
1299
  self.sublinear_stride = max(0, int(sublinear_stride))
1300
  self.sublinear_max_anchors = max(0, int(sublinear_max_anchors))
1301
  self.sublinear_chunk = max(1, int(sublinear_chunk))
 
 
 
 
 
 
1302
  # Exact n1 harvest: one fused QKV projection is mathematically the same
1303
  # as three independent bias-free Linear(d, d) projections with their
1304
  # weights stacked along out_features.
@@ -1395,6 +1407,29 @@ class TuneableAttentionMHA(nn.Module):
1395
  return (idx // block) <= (q_pos[:, None] // block)
1396
  raise ValueError(f"unknown structured attention mask kind: {kind}")
1397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1398
  def _sublinear_attention(self, q, k, v, attn_mask=None, rel_bias_tokens=None):
1399
  """Local-window + landmark attention: O(N * (window + N/stride))."""
1400
  bsz, heads, q_len, _ = q.shape
@@ -1407,25 +1442,20 @@ class TuneableAttentionMHA(nn.Module):
1407
  if self.use_relpos and rel_bias_tokens is not None:
1408
  slopes = _alibi_slopes(self.h).to(device=device, dtype=torch.float32)
1409
 
1410
- anchor_start = self.sublinear_stride - 1
1411
- if self.sublinear_stride > 0 and self.sublinear_max_anchors > 0 and anchor_start < k_len:
1412
- anchors = torch.arange(
1413
- anchor_start,
1414
- k_len,
1415
- self.sublinear_stride,
1416
- device=device,
1417
- dtype=torch.long,
1418
- )
1419
- if anchors.numel() > self.sublinear_max_anchors:
1420
- # even-coverage: span the WHOLE past at fixed budget (not just recent tail)
1421
- _sel = torch.linspace(0, anchors.numel() - 1, self.sublinear_max_anchors, device=device).round().long().unique()
1422
- anchors = anchors[_sel]
1423
- else:
1424
- anchors = torch.empty(0, device=device, dtype=torch.long)
1425
- # attention sinks: always keep the first few tokens (StreamingLLM)
1426
- _sink = int(getattr(self, "sublinear_sinks", 4))
1427
- if _sink > 0 and k_len > 0:
1428
- anchors = torch.cat([torch.arange(min(_sink, k_len), device=device, dtype=torch.long), anchors]).unique()
1429
 
1430
  offsets = torch.arange(
1431
  -self.sublinear_window,
@@ -1443,24 +1473,38 @@ class TuneableAttentionMHA(nn.Module):
1443
  local_valid = (local_raw >= 0) & (local_raw < k_len)
1444
  local_idx = local_raw.clamp(0, max(0, k_len - 1))
1445
 
 
 
1446
  if anchors.numel():
1447
  anchor_idx = anchors.view(1, -1).expand(cur, -1)
1448
- anchor_valid = torch.ones_like(anchor_idx, dtype=torch.bool)
 
 
 
1449
  idx = torch.cat([local_idx, anchor_idx], dim=1)
1450
  valid = torch.cat([local_valid, anchor_valid], dim=1)
 
 
 
 
 
 
 
 
1451
  else:
1452
  idx = local_idx
1453
  valid = local_valid
 
 
1454
 
1455
  structured_valid = self._structured_valid(attn_mask, q_pos, idx)
1456
  if structured_valid is not None:
1457
  valid = valid & structured_valid
1458
 
1459
- k_sel = k[:, :, idx, :]
1460
  scores = (q[:, :, q_start:q_end, :].unsqueeze(-2) * k_sel).sum(dim=-1) * scale
1461
 
1462
  if slopes is not None:
1463
- dist = (idx.view(1, 1, cur, -1) - q_pos.view(1, 1, cur, 1)).clamp_min(0).to(torch.float32)
1464
  scores = scores + (-slopes * dist).to(scores.dtype)
1465
 
1466
  if torch.is_tensor(attn_mask) and attn_mask.size(-1) == k_len and attn_mask.size(-2) >= q_end:
@@ -1470,7 +1514,6 @@ class TuneableAttentionMHA(nn.Module):
1470
 
1471
  scores = scores.masked_fill(~valid.view(1, 1, cur, -1), float("-inf"))
1472
  weights = torch.softmax(scores.float(), dim=-1).to(v.dtype)
1473
- v_sel = v[:, :, idx, :]
1474
  outputs.append((weights.unsqueeze(-1) * v_sel).sum(dim=-2))
1475
 
1476
  return torch.cat(outputs, dim=2)
@@ -1544,6 +1587,9 @@ class Block(nn.Module):
1544
  sublinear_stride: int = DEFAULT_SUBLINEAR_STRIDE,
1545
  sublinear_max_anchors: int = DEFAULT_SUBLINEAR_MAX_ANCHORS,
1546
  sublinear_chunk: int = DEFAULT_SUBLINEAR_CHUNK,
 
 
 
1547
  ):
1548
  super().__init__()
1549
  self.ln1, self.ln2 = nn.LayerNorm(d), nn.LayerNorm(d)
@@ -1556,6 +1602,9 @@ class Block(nn.Module):
1556
  sublinear_stride=sublinear_stride,
1557
  sublinear_max_anchors=sublinear_max_anchors,
1558
  sublinear_chunk=sublinear_chunk,
 
 
 
1559
  )
1560
  self.ff = nn.Sequential(nn.Linear(d, 4 * d), nn.ReLU(), nn.Linear(4 * d, d))
1561
 
@@ -1581,6 +1630,9 @@ class Encoder(nn.Module):
1581
  sublinear_stride: int = DEFAULT_SUBLINEAR_STRIDE,
1582
  sublinear_max_anchors: int = DEFAULT_SUBLINEAR_MAX_ANCHORS,
1583
  sublinear_chunk: int = DEFAULT_SUBLINEAR_CHUNK,
 
 
 
1584
  anchor_memory: bool = DEFAULT_ANCHOR_MEMORY,
1585
  anchor_stride: int = DEFAULT_ANCHOR_STRIDE,
1586
  anchor_max: int = DEFAULT_ANCHOR_MAX,
@@ -1599,6 +1651,9 @@ class Encoder(nn.Module):
1599
  sublinear_stride=sublinear_stride,
1600
  sublinear_max_anchors=sublinear_max_anchors,
1601
  sublinear_chunk=sublinear_chunk,
 
 
 
1602
  )
1603
  for _ in range(l)
1604
  ])
@@ -1610,6 +1665,9 @@ class Encoder(nn.Module):
1610
  self.sublinear_stride = sublinear_stride
1611
  self.sublinear_max_anchors = sublinear_max_anchors
1612
  self.sublinear_chunk = sublinear_chunk
 
 
 
1613
  self.anchor_memory_enabled = bool(anchor_memory)
1614
  self.anchor_stride = int(anchor_stride)
1615
  self.anchor_max = int(anchor_max)
@@ -2487,7 +2545,9 @@ def train(args):
2487
  "AGILLM-4 runtime: "
2488
  f"attn_backend={args.attn_backend} grad_checkpoint={args.grad_checkpoint} "
2489
  f"sublinear_window={args.sublinear_window} sublinear_stride={args.sublinear_stride} "
2490
- f"sublinear_max_anchors={args.sublinear_max_anchors} sublinear_chunk={args.sublinear_chunk}"
 
 
2491
  )
2492
  core = Encoder(
2493
  cfg,
@@ -2498,6 +2558,9 @@ def train(args):
2498
  sublinear_stride=args.sublinear_stride,
2499
  sublinear_max_anchors=args.sublinear_max_anchors,
2500
  sublinear_chunk=args.sublinear_chunk,
 
 
 
2501
  anchor_memory=getattr(args, "anchor_memory", DEFAULT_ANCHOR_MEMORY),
2502
  anchor_stride=getattr(args, "anchor_stride", DEFAULT_ANCHOR_STRIDE),
2503
  anchor_max=getattr(args, "anchor_max", DEFAULT_ANCHOR_MAX),
@@ -2717,6 +2780,9 @@ def infer(args):
2717
  sublinear_stride=args.sublinear_stride,
2718
  sublinear_max_anchors=args.sublinear_max_anchors,
2719
  sublinear_chunk=args.sublinear_chunk,
 
 
 
2720
  anchor_memory=getattr(args, "anchor_memory", DEFAULT_ANCHOR_MEMORY),
2721
  anchor_stride=getattr(args, "anchor_stride", DEFAULT_ANCHOR_STRIDE),
2722
  anchor_max=getattr(args, "anchor_max", DEFAULT_ANCHOR_MAX),
@@ -2888,6 +2954,13 @@ def main():
2888
  help="For --attn_backend sublinear, cap landmark candidates per query chunk.")
2889
  tr.add_argument("--sublinear_chunk", type=int, default=DEFAULT_SUBLINEAR_CHUNK,
2890
  help="For --attn_backend sublinear, query chunk size controlling peak gather memory.")
 
 
 
 
 
 
 
2891
  tr.add_argument("--no_structured_masks", action="store_true",
2892
  help="Disable structured causal/SAT masks for sublinear attention and fall back to dense masks.")
2893
  tr.add_argument("--anchor_memory", action="store_true",
@@ -3009,6 +3082,10 @@ def main():
3009
  inf.add_argument("--sublinear_stride", type=int, default=DEFAULT_SUBLINEAR_STRIDE)
3010
  inf.add_argument("--sublinear_max_anchors", type=int, default=DEFAULT_SUBLINEAR_MAX_ANCHORS)
3011
  inf.add_argument("--sublinear_chunk", type=int, default=DEFAULT_SUBLINEAR_CHUNK)
 
 
 
 
3012
  inf.add_argument("--no_structured_masks", action="store_true")
3013
  inf.add_argument("--nat_expand", type=int, default=2)
3014
  inf.add_argument("--nat_passes", type=int, default=1)
 
926
  DEFAULT_SUBLINEAR_STRIDE = _env_int("AGILLM_SUBLINEAR_STRIDE", 64)
927
  DEFAULT_SUBLINEAR_MAX_ANCHORS = _env_int("AGILLM_SUBLINEAR_MAX_ANCHORS", 256)
928
  DEFAULT_SUBLINEAR_CHUNK = _env_int("AGILLM_SUBLINEAR_CHUNK", 128)
929
+ DEFAULT_SUBLINEAR_SINKS = _env_int("AGILLM_SUBLINEAR_SINKS", 4)
930
+ DEFAULT_SUBLINEAR_RECENT_ANCHORS = _env_int("AGILLM_SUBLINEAR_RECENT_ANCHORS", -1) # -1 = half of max anchors
931
+ DEFAULT_SUBLINEAR_POOLED_LANDMARKS = bool(_env_int("AGILLM_SUBLINEAR_POOLED_LANDMARKS", 0))
932
  DEFAULT_ANCHOR_MEMORY = bool(_env_int("AGILLM_ANCHOR_MEMORY", 0))
933
  DEFAULT_ANCHOR_STRIDE = _env_int("AGILLM_ANCHOR_STRIDE", 256)
934
  DEFAULT_ANCHOR_MAX = _env_int("AGILLM_ANCHOR_MAX", 2048)
 
1292
  sublinear_stride: int = DEFAULT_SUBLINEAR_STRIDE,
1293
  sublinear_max_anchors: int = DEFAULT_SUBLINEAR_MAX_ANCHORS,
1294
  sublinear_chunk: int = DEFAULT_SUBLINEAR_CHUNK,
1295
+ sublinear_sinks: int = DEFAULT_SUBLINEAR_SINKS,
1296
+ sublinear_recent_anchors: int = DEFAULT_SUBLINEAR_RECENT_ANCHORS,
1297
+ sublinear_pooled_landmarks: bool = DEFAULT_SUBLINEAR_POOLED_LANDMARKS,
1298
  ):
1299
  super().__init__()
1300
  assert d % h == 0
 
1305
  self.sublinear_stride = max(0, int(sublinear_stride))
1306
  self.sublinear_max_anchors = max(0, int(sublinear_max_anchors))
1307
  self.sublinear_chunk = max(1, int(sublinear_chunk))
1308
+ self.sublinear_sinks = max(0, int(sublinear_sinks))
1309
+ recent = int(sublinear_recent_anchors)
1310
+ if recent < 0:
1311
+ recent = self.sublinear_max_anchors // 2
1312
+ self.sublinear_recent_anchors = min(max(0, recent), self.sublinear_max_anchors)
1313
+ self.sublinear_pooled_landmarks = bool(sublinear_pooled_landmarks)
1314
  # Exact n1 harvest: one fused QKV projection is mathematically the same
1315
  # as three independent bias-free Linear(d, d) projections with their
1316
  # weights stacked along out_features.
 
1407
  return (idx // block) <= (q_pos[:, None] // block)
1408
  raise ValueError(f"unknown structured attention mask kind: {kind}")
1409
 
1410
+ def _sublinear_anchor_positions(self, k_len: int, device):
1411
+ anchor_start = self.sublinear_stride - 1
1412
+ if self.sublinear_stride <= 0 or self.sublinear_max_anchors <= 0 or anchor_start >= k_len:
1413
+ anchors = torch.empty(0, device=device, dtype=torch.long)
1414
+ else:
1415
+ all_anchors = torch.arange(anchor_start, k_len, self.sublinear_stride, device=device, dtype=torch.long)
1416
+ if all_anchors.numel() <= self.sublinear_max_anchors:
1417
+ anchors = all_anchors
1418
+ else:
1419
+ recent_budget = min(self.sublinear_recent_anchors, self.sublinear_max_anchors)
1420
+ span_budget = max(0, self.sublinear_max_anchors - recent_budget)
1421
+ parts = []
1422
+ if span_budget > 0:
1423
+ span_sel = torch.linspace(0, all_anchors.numel() - 1, span_budget, device=device).round().long().unique()
1424
+ parts.append(all_anchors[span_sel])
1425
+ if recent_budget > 0:
1426
+ parts.append(all_anchors[-recent_budget:])
1427
+ anchors = torch.cat(parts).unique() if parts else torch.empty(0, device=device, dtype=torch.long)
1428
+ if self.sublinear_sinks > 0 and k_len > 0:
1429
+ sinks = torch.arange(min(self.sublinear_sinks, k_len), device=device, dtype=torch.long)
1430
+ anchors = torch.cat([sinks, anchors]).unique() if anchors.numel() else sinks
1431
+ return anchors
1432
+
1433
  def _sublinear_attention(self, q, k, v, attn_mask=None, rel_bias_tokens=None):
1434
  """Local-window + landmark attention: O(N * (window + N/stride))."""
1435
  bsz, heads, q_len, _ = q.shape
 
1442
  if self.use_relpos and rel_bias_tokens is not None:
1443
  slopes = _alibi_slopes(self.h).to(device=device, dtype=torch.float32)
1444
 
1445
+ anchors = self._sublinear_anchor_positions(k_len, device)
1446
+ anchor_k = anchor_v = None
1447
+ if anchors.numel() and self.sublinear_pooled_landmarks and self.sublinear_stride > 1:
1448
+ # Optional pooled landmarks: each global anchor summarizes its stride segment.
1449
+ # This is off by default because it adds cumsum work; enable after benchmarking.
1450
+ ends = anchors + 1
1451
+ starts = (ends - self.sublinear_stride).clamp_min(0)
1452
+ zero_k = k.new_zeros(k.size(0), k.size(1), 1, k.size(3))
1453
+ zero_v = v.new_zeros(v.size(0), v.size(1), 1, v.size(3))
1454
+ prefix_k = torch.cat([zero_k, k.cumsum(dim=2)], dim=2)
1455
+ prefix_v = torch.cat([zero_v, v.cumsum(dim=2)], dim=2)
1456
+ denom = (ends - starts).to(dtype=k.dtype).view(1, 1, -1, 1).clamp_min(1)
1457
+ anchor_k = (prefix_k[:, :, ends, :] - prefix_k[:, :, starts, :]) / denom
1458
+ anchor_v = (prefix_v[:, :, ends, :] - prefix_v[:, :, starts, :]) / denom
 
 
 
 
 
1459
 
1460
  offsets = torch.arange(
1461
  -self.sublinear_window,
 
1473
  local_valid = (local_raw >= 0) & (local_raw < k_len)
1474
  local_idx = local_raw.clamp(0, max(0, k_len - 1))
1475
 
1476
+ k_local = k[:, :, local_idx, :]
1477
+ v_local = v[:, :, local_idx, :]
1478
  if anchors.numel():
1479
  anchor_idx = anchors.view(1, -1).expand(cur, -1)
1480
+ local_lo = (q_pos - self.sublinear_window).clamp_min(0).view(-1, 1)
1481
+ local_hi = (q_pos + self.sublinear_window).clamp_max(max(0, k_len - 1)).view(-1, 1)
1482
+ # Drop anchor copies already present in the local window; duplicates bias softmax mass.
1483
+ anchor_valid = (anchor_idx < local_lo) | (anchor_idx > local_hi)
1484
  idx = torch.cat([local_idx, anchor_idx], dim=1)
1485
  valid = torch.cat([local_valid, anchor_valid], dim=1)
1486
+ if anchor_k is not None and anchor_v is not None:
1487
+ k_anchor = anchor_k.unsqueeze(2).expand(-1, -1, cur, -1, -1)
1488
+ v_anchor = anchor_v.unsqueeze(2).expand(-1, -1, cur, -1, -1)
1489
+ else:
1490
+ k_anchor = k[:, :, anchor_idx, :]
1491
+ v_anchor = v[:, :, anchor_idx, :]
1492
+ k_sel = torch.cat([k_local, k_anchor], dim=-2)
1493
+ v_sel = torch.cat([v_local, v_anchor], dim=-2)
1494
  else:
1495
  idx = local_idx
1496
  valid = local_valid
1497
+ k_sel = k_local
1498
+ v_sel = v_local
1499
 
1500
  structured_valid = self._structured_valid(attn_mask, q_pos, idx)
1501
  if structured_valid is not None:
1502
  valid = valid & structured_valid
1503
 
 
1504
  scores = (q[:, :, q_start:q_end, :].unsqueeze(-2) * k_sel).sum(dim=-1) * scale
1505
 
1506
  if slopes is not None:
1507
+ dist = (q_pos.view(1, 1, cur, 1) - idx.view(1, 1, cur, -1)).abs().to(torch.float32)
1508
  scores = scores + (-slopes * dist).to(scores.dtype)
1509
 
1510
  if torch.is_tensor(attn_mask) and attn_mask.size(-1) == k_len and attn_mask.size(-2) >= q_end:
 
1514
 
1515
  scores = scores.masked_fill(~valid.view(1, 1, cur, -1), float("-inf"))
1516
  weights = torch.softmax(scores.float(), dim=-1).to(v.dtype)
 
1517
  outputs.append((weights.unsqueeze(-1) * v_sel).sum(dim=-2))
1518
 
1519
  return torch.cat(outputs, dim=2)
 
1587
  sublinear_stride: int = DEFAULT_SUBLINEAR_STRIDE,
1588
  sublinear_max_anchors: int = DEFAULT_SUBLINEAR_MAX_ANCHORS,
1589
  sublinear_chunk: int = DEFAULT_SUBLINEAR_CHUNK,
1590
+ sublinear_sinks: int = DEFAULT_SUBLINEAR_SINKS,
1591
+ sublinear_recent_anchors: int = DEFAULT_SUBLINEAR_RECENT_ANCHORS,
1592
+ sublinear_pooled_landmarks: bool = DEFAULT_SUBLINEAR_POOLED_LANDMARKS,
1593
  ):
1594
  super().__init__()
1595
  self.ln1, self.ln2 = nn.LayerNorm(d), nn.LayerNorm(d)
 
1602
  sublinear_stride=sublinear_stride,
1603
  sublinear_max_anchors=sublinear_max_anchors,
1604
  sublinear_chunk=sublinear_chunk,
1605
+ sublinear_sinks=sublinear_sinks,
1606
+ sublinear_recent_anchors=sublinear_recent_anchors,
1607
+ sublinear_pooled_landmarks=sublinear_pooled_landmarks,
1608
  )
1609
  self.ff = nn.Sequential(nn.Linear(d, 4 * d), nn.ReLU(), nn.Linear(4 * d, d))
1610
 
 
1630
  sublinear_stride: int = DEFAULT_SUBLINEAR_STRIDE,
1631
  sublinear_max_anchors: int = DEFAULT_SUBLINEAR_MAX_ANCHORS,
1632
  sublinear_chunk: int = DEFAULT_SUBLINEAR_CHUNK,
1633
+ sublinear_sinks: int = DEFAULT_SUBLINEAR_SINKS,
1634
+ sublinear_recent_anchors: int = DEFAULT_SUBLINEAR_RECENT_ANCHORS,
1635
+ sublinear_pooled_landmarks: bool = DEFAULT_SUBLINEAR_POOLED_LANDMARKS,
1636
  anchor_memory: bool = DEFAULT_ANCHOR_MEMORY,
1637
  anchor_stride: int = DEFAULT_ANCHOR_STRIDE,
1638
  anchor_max: int = DEFAULT_ANCHOR_MAX,
 
1651
  sublinear_stride=sublinear_stride,
1652
  sublinear_max_anchors=sublinear_max_anchors,
1653
  sublinear_chunk=sublinear_chunk,
1654
+ sublinear_sinks=sublinear_sinks,
1655
+ sublinear_recent_anchors=sublinear_recent_anchors,
1656
+ sublinear_pooled_landmarks=sublinear_pooled_landmarks,
1657
  )
1658
  for _ in range(l)
1659
  ])
 
1665
  self.sublinear_stride = sublinear_stride
1666
  self.sublinear_max_anchors = sublinear_max_anchors
1667
  self.sublinear_chunk = sublinear_chunk
1668
+ self.sublinear_sinks = sublinear_sinks
1669
+ self.sublinear_recent_anchors = sublinear_recent_anchors
1670
+ self.sublinear_pooled_landmarks = bool(sublinear_pooled_landmarks)
1671
  self.anchor_memory_enabled = bool(anchor_memory)
1672
  self.anchor_stride = int(anchor_stride)
1673
  self.anchor_max = int(anchor_max)
 
2545
  "AGILLM-4 runtime: "
2546
  f"attn_backend={args.attn_backend} grad_checkpoint={args.grad_checkpoint} "
2547
  f"sublinear_window={args.sublinear_window} sublinear_stride={args.sublinear_stride} "
2548
+ f"sublinear_max_anchors={args.sublinear_max_anchors} sublinear_chunk={args.sublinear_chunk} "
2549
+ f"sublinear_sinks={args.sublinear_sinks} sublinear_recent_anchors={args.sublinear_recent_anchors} "
2550
+ f"sublinear_pooled_landmarks={args.sublinear_pooled_landmarks}"
2551
  )
2552
  core = Encoder(
2553
  cfg,
 
2558
  sublinear_stride=args.sublinear_stride,
2559
  sublinear_max_anchors=args.sublinear_max_anchors,
2560
  sublinear_chunk=args.sublinear_chunk,
2561
+ sublinear_sinks=args.sublinear_sinks,
2562
+ sublinear_recent_anchors=args.sublinear_recent_anchors,
2563
+ sublinear_pooled_landmarks=args.sublinear_pooled_landmarks,
2564
  anchor_memory=getattr(args, "anchor_memory", DEFAULT_ANCHOR_MEMORY),
2565
  anchor_stride=getattr(args, "anchor_stride", DEFAULT_ANCHOR_STRIDE),
2566
  anchor_max=getattr(args, "anchor_max", DEFAULT_ANCHOR_MAX),
 
2780
  sublinear_stride=args.sublinear_stride,
2781
  sublinear_max_anchors=args.sublinear_max_anchors,
2782
  sublinear_chunk=args.sublinear_chunk,
2783
+ sublinear_sinks=args.sublinear_sinks,
2784
+ sublinear_recent_anchors=args.sublinear_recent_anchors,
2785
+ sublinear_pooled_landmarks=args.sublinear_pooled_landmarks,
2786
  anchor_memory=getattr(args, "anchor_memory", DEFAULT_ANCHOR_MEMORY),
2787
  anchor_stride=getattr(args, "anchor_stride", DEFAULT_ANCHOR_STRIDE),
2788
  anchor_max=getattr(args, "anchor_max", DEFAULT_ANCHOR_MAX),
 
2954
  help="For --attn_backend sublinear, cap landmark candidates per query chunk.")
2955
  tr.add_argument("--sublinear_chunk", type=int, default=DEFAULT_SUBLINEAR_CHUNK,
2956
  help="For --attn_backend sublinear, query chunk size controlling peak gather memory.")
2957
+ tr.add_argument("--sublinear_sinks", type=int, default=DEFAULT_SUBLINEAR_SINKS,
2958
+ help="For sublinear attention, always include this many first-token attention sinks.")
2959
+ tr.add_argument("--sublinear_recent_anchors", type=int, default=DEFAULT_SUBLINEAR_RECENT_ANCHORS,
2960
+ help="For capped sublinear anchors, reserve this many anchors for the recent tail; -1 uses half.")
2961
+ tr.add_argument("--sublinear_pooled_landmarks", action=argparse.BooleanOptionalAction,
2962
+ default=DEFAULT_SUBLINEAR_POOLED_LANDMARKS,
2963
+ help="Use stride-segment pooled K/V summaries for sublinear landmark anchors.")
2964
  tr.add_argument("--no_structured_masks", action="store_true",
2965
  help="Disable structured causal/SAT masks for sublinear attention and fall back to dense masks.")
2966
  tr.add_argument("--anchor_memory", action="store_true",
 
3082
  inf.add_argument("--sublinear_stride", type=int, default=DEFAULT_SUBLINEAR_STRIDE)
3083
  inf.add_argument("--sublinear_max_anchors", type=int, default=DEFAULT_SUBLINEAR_MAX_ANCHORS)
3084
  inf.add_argument("--sublinear_chunk", type=int, default=DEFAULT_SUBLINEAR_CHUNK)
3085
+ inf.add_argument("--sublinear_sinks", type=int, default=DEFAULT_SUBLINEAR_SINKS)
3086
+ inf.add_argument("--sublinear_recent_anchors", type=int, default=DEFAULT_SUBLINEAR_RECENT_ANCHORS)
3087
+ inf.add_argument("--sublinear_pooled_landmarks", action=argparse.BooleanOptionalAction,
3088
+ default=DEFAULT_SUBLINEAR_POOLED_LANDMARKS)
3089
  inf.add_argument("--no_structured_masks", action="store_true")
3090
  inf.add_argument("--nat_expand", type=int, default=2)
3091
  inf.add_argument("--nat_passes", type=int, default=1)
relaunch_agillm4_dblock_sg2.sh ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Relaunch AGILLM-4 dblock with SG2's tuned config + improved sublinear attention v2.
3
+ set -Eeuo pipefail
4
+ cd /workspace/agillm-4
5
+ export TOKENIZERS_PARALLELISM=false
6
+ export TOKENIZER_ID="${TOKENIZER_ID:-deepseek-ai/DeepSeek-V4-Pro}"
7
+ export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512,expandable_segments:True
8
+ export AGILLM_ATTN_BACKEND=sublinear
9
+ [ -f /root/.cache/huggingface/token ] && { export HF_TOKEN="$(tr -d '\r\n' </root/.cache/huggingface/token)"; export HUGGING_FACE_HUB_TOKEN="$HF_TOKEN"; }
10
+ SAVE_DIR=/workspace/agillm4_4090_ckpts
11
+ CKPT="$(ls -1t "$SAVE_DIR"/pretrain_step*.pt 2>/dev/null | head -1)"
12
+ exec >> /workspace/agillm4_floor_train.log 2>&1
13
+ echo "RELAUNCH_AGILLM4_DBLOCK_SG2 $(date -u +%Y-%m-%dT%H:%M:%SZ) resume=$CKPT (improved sublinear v2: ALiBi distance + dedupe + hybrid anchors + sinks)"
14
+ exec python -u nB300_agillm4.py train --preset agillm4_floor --resume "$CKPT" \
15
+ --dblock --dblock_blocks 4 --dblock_schedule loss_balanced --dblock_warmup_steps 16 \
16
+ --dblock_sigma_curriculum_steps 2000 --dblock_log_every 25 --dblock_objective_mode stochastic \
17
+ --dblock_ar_prob 0.85 --dblock_sat_prob 0.075 --dblock_nat_prob 0.075 \
18
+ --dblock_ar_loss_tokens 512 --dblock_sat_loss_tokens 0 --dblock_nat_loss_tokens 512 \
19
+ --tie_weights --batch_size 2 --block 1280 --amp --attn_backend sublinear \
20
+ --sublinear_window 128 --sublinear_stride 128 --sublinear_max_anchors 128 --sublinear_chunk 128 \
21
+ --sublinear_sinks 4 --sublinear_recent_anchors 64 --no-sublinear_pooled_landmarks \
22
+ --grad_checkpoint --optimizer paged_adamw8bit --sat_every 4 --nat_every 4 --nat_max_tokens 768 --nat_mask_ratio 0.5 \
23
+ --token_param_ratio 100 --save_dir "$SAVE_DIR" --save_every_sec 86400 --heartbeat_every_sec 300 \
24
+ --empty_cache_every_steps 0 --delta_every_steps 25000 --delta_max_keep 1 --max_ckpts 1
sublinear_improved.py CHANGED
@@ -1,54 +1,58 @@
1
  """Improved sublinear-attention anchor selection for AGILLM-4.
2
 
3
- AGILLM-4's `sublinear` attention = local sliding window + strided "landmark"
4
- anchors. The original capped the anchor set with `anchors[-max_anchors:]`, which
5
- DROPS the entire deep past once N > max_anchors*stride (the trainer goes blind to
6
- everything older than the recent tail). This patch keeps whole-sequence coverage at
7
- the SAME key budget, plus StreamingLLM-style attention sinks.
8
-
9
- Drop-in replacement for the anchor-cap block inside MHA._sublinear_attention.
10
  """
11
  import torch
12
 
13
- def select_anchors(k_len, stride, max_anchors, sinks, device):
14
- """Even-coverage strided landmarks over the FULL past + attention sinks."""
 
 
15
  start = stride - 1
16
- if stride > 0 and max_anchors > 0 and start < k_len:
17
- anchors = torch.arange(start, k_len, stride, device=device, dtype=torch.long)
18
- if anchors.numel() > max_anchors:
19
- # even-coverage subsample across the whole sequence (NOT the recent tail)
20
- sel = torch.linspace(0, anchors.numel() - 1, max_anchors, device=device).round().long().unique()
21
- anchors = anchors[sel]
22
- else:
23
  anchors = torch.empty(0, device=device, dtype=torch.long)
24
- if sinks > 0 and k_len > 0: # always keep the first few tokens
25
- anchors = torch.cat([torch.arange(min(sinks, k_len), device=device, dtype=torch.long), anchors]).unique()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  return anchors
27
 
28
- # --- exact patch applied to nB300_agillm4.py MHA._sublinear_attention ---
29
- PATCH = '''
30
- # replace:
31
- # if anchors.numel() > self.sublinear_max_anchors:
32
- # anchors = anchors[-self.sublinear_max_anchors :]
33
- # with:
34
- if anchors.numel() > self.sublinear_max_anchors:
35
- _sel = torch.linspace(0, anchors.numel() - 1, self.sublinear_max_anchors, device=device).round().long().unique()
36
- anchors = anchors[_sel]
37
- # ... and after the else branch add:
38
- _sink = int(getattr(self, "sublinear_sinks", 4))
39
- if _sink > 0 and k_len > 0:
40
- anchors = torch.cat([torch.arange(min(_sink, k_len), device=device, dtype=torch.long), anchors]).unique()
41
- '''
42
 
43
- if __name__ == "__main__":
44
- # Structural coverage demo at the live config, N beyond max_anchors*stride.
45
- N, W, stride, maxA, sinks = 32768, 128, 128, 128, 4
46
- i = N - 1
47
- loc = set(range(i - W, i + 1))
48
- allA = list(range(stride - 1, i + 1, stride))
49
- OLD = sorted(loc | set(allA[-maxA:]))
50
- sel = torch.linspace(0, len(allA) - 1, maxA).round().long().tolist()
51
- NEW = sorted(loc | {allA[s] for s in sel} | set(range(sinks)))
52
- print(f"N={N} (cap bites: {len(allA)} anchors > {maxA})")
53
- print(f"OLD covers {min(OLD)}..{max(OLD)} -> blind to 0..{min(OLD)-1}; first-half keys={sum(x<N//2 for x in OLD)}")
54
- print(f"NEW covers {min(NEW)}..{max(NEW)} -> full span; first-half keys={sum(x<N//2 for x in NEW)}")
 
 
 
 
 
 
 
1
  """Improved sublinear-attention anchor selection for AGILLM-4.
2
 
3
+ V2 used by the live AGILLM-4 DBlock line:
4
+ - fixes gathered ALiBi distance so past causal keys receive distance penalty
5
+ - suppresses local/anchor duplicate candidates before softmax
6
+ - uses hybrid full-span + recent-tail anchors under the same max-anchor budget
7
+ - exposes first-token attention sinks as `--sublinear_sinks`
8
+ - includes optional pooled landmark K/V summaries behind `--sublinear_pooled_landmarks`
 
9
  """
10
  import torch
11
 
12
+
13
+ def select_hybrid_anchors(k_len, stride, max_anchors, sinks=4, recent_anchors=-1, device='cpu'):
14
+ """Full-span + recent-tail landmark positions, plus attention sinks."""
15
+ device = torch.device(device)
16
  start = stride - 1
17
+ if stride <= 0 or max_anchors <= 0 or start >= k_len:
 
 
 
 
 
 
18
  anchors = torch.empty(0, device=device, dtype=torch.long)
19
+ else:
20
+ all_anchors = torch.arange(start, k_len, stride, device=device, dtype=torch.long)
21
+ if all_anchors.numel() <= max_anchors:
22
+ anchors = all_anchors
23
+ else:
24
+ if recent_anchors < 0:
25
+ recent_anchors = max_anchors // 2
26
+ recent_budget = min(max(0, int(recent_anchors)), max_anchors)
27
+ span_budget = max(0, max_anchors - recent_budget)
28
+ parts = []
29
+ if span_budget > 0:
30
+ sel = torch.linspace(0, all_anchors.numel() - 1, span_budget, device=device).round().long().unique()
31
+ parts.append(all_anchors[sel])
32
+ if recent_budget > 0:
33
+ parts.append(all_anchors[-recent_budget:])
34
+ anchors = torch.cat(parts).unique() if parts else torch.empty(0, device=device, dtype=torch.long)
35
+ if sinks > 0 and k_len > 0:
36
+ sink_idx = torch.arange(min(int(sinks), k_len), device=device, dtype=torch.long)
37
+ anchors = torch.cat([sink_idx, anchors]).unique() if anchors.numel() else sink_idx
38
  return anchors
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ def local_anchor_valid(q_pos, anchors, window, k_len):
42
+ """False where an anchor is already inside that query's local window."""
43
+ anchor_idx = anchors.view(1, -1).expand(q_pos.numel(), -1)
44
+ local_lo = (q_pos - window).clamp_min(0).view(-1, 1)
45
+ local_hi = (q_pos + window).clamp_max(max(0, k_len - 1)).view(-1, 1)
46
+ return (anchor_idx < local_lo) | (anchor_idx > local_hi)
47
+
48
+
49
+ if __name__ == '__main__':
50
+ N, window, stride, maxA, sinks, recent = 32768, 128, 128, 128, 4, 64
51
+ old_all = list(range(stride - 1, N, stride))
52
+ old = sorted(set(range(N - window - 1, N)) | set(old_all[-maxA:]))
53
+ new = select_hybrid_anchors(N, stride, maxA, sinks, recent).tolist()
54
+ print(f'N={N} stride={stride} maxA={maxA} sinks={sinks} recent={recent}')
55
+ print(f'OLD anchor coverage: {min(old)}..{max(old)} first_half={sum(x < N//2 for x in old)}')
56
+ print(f'NEW anchor coverage: {min(new)}..{max(new)} first_half={sum(x < N//2 for x in new)} recent_tail={sum(x >= N-8192 for x in new)}')
57
+ q = torch.tensor([N - 1])
58
+ print(f'duplicate-suppressed anchors for final query: {int(local_anchor_valid(q, torch.tensor(new), window, N).sum())}/{len(new)}')
sublinear_improved_snippet.py CHANGED
@@ -1,35 +1,29 @@
1
- """Improved AGILLM-4 sublinear attention anchor selection.
2
 
3
- Drop this block into `_sublinear_attention` in place of the recent-tail anchor
4
- selection. It keeps the same local-window + anchor key budget shape, but avoids
5
- losing the deep past once `num_anchors > sublinear_max_anchors`.
6
  """
7
 
8
- anchor_start = self.sublinear_stride - 1
9
- if self.sublinear_stride > 0 and self.sublinear_max_anchors > 0 and anchor_start < k_len:
10
- anchors = torch.arange(
11
- anchor_start,
12
- k_len,
13
- self.sublinear_stride,
14
- device=device,
15
- dtype=torch.long,
16
- )
17
- if anchors.numel() > self.sublinear_max_anchors:
18
- # Span the whole sequence instead of keeping only the recent tail.
19
- sel = torch.linspace(
20
- 0,
21
- anchors.numel() - 1,
22
- self.sublinear_max_anchors,
23
- device=device,
24
- ).round().long().unique()
25
- anchors = anchors[sel]
26
- else:
27
- anchors = torch.empty(0, device=device, dtype=torch.long)
28
 
29
- # StreamingLLM-style attention sinks: preserve the first tokens as stable global memory.
30
- sink = int(getattr(self, "sublinear_sinks", 4))
31
- if sink > 0 and k_len > 0:
32
- anchors = torch.cat([
33
- torch.arange(min(sink, k_len), device=device, dtype=torch.long),
34
- anchors,
35
- ]).unique()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal AGILLM-4 sublinear attention V2 snippets.
2
 
3
+ These are the core blocks now folded into `nB300_agillm4_vram_dblock.py`.
 
 
4
  """
5
 
6
+ # Anchor selection: full-span + recent-tail + sinks.
7
+ anchors = self._sublinear_anchor_positions(k_len, device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ # Optional pooled landmarks are available behind --sublinear_pooled_landmarks.
10
+ if anchors.numel() and self.sublinear_pooled_landmarks and self.sublinear_stride > 1:
11
+ ends = anchors + 1
12
+ starts = (ends - self.sublinear_stride).clamp_min(0)
13
+ zero_k = k.new_zeros(k.size(0), k.size(1), 1, k.size(3))
14
+ zero_v = v.new_zeros(v.size(0), v.size(1), 1, v.size(3))
15
+ prefix_k = torch.cat([zero_k, k.cumsum(dim=2)], dim=2)
16
+ prefix_v = torch.cat([zero_v, v.cumsum(dim=2)], dim=2)
17
+ denom = (ends - starts).to(dtype=k.dtype).view(1, 1, -1, 1).clamp_min(1)
18
+ anchor_k = (prefix_k[:, :, ends, :] - prefix_k[:, :, starts, :]) / denom
19
+ anchor_v = (prefix_v[:, :, ends, :] - prefix_v[:, :, starts, :]) / denom
20
+
21
+ # Duplicate suppression: do not let an anchor double-count a key already in local attention.
22
+ anchor_idx = anchors.view(1, -1).expand(cur, -1)
23
+ local_lo = (q_pos - self.sublinear_window).clamp_min(0).view(-1, 1)
24
+ local_hi = (q_pos + self.sublinear_window).clamp_max(max(0, k_len - 1)).view(-1, 1)
25
+ anchor_valid = (anchor_idx < local_lo) | (anchor_idx > local_hi)
26
+
27
+ # Gathered ALiBi distance: distance from query to selected key, not future-only clamp.
28
+ dist = (q_pos.view(1, 1, cur, 1) - idx.view(1, 1, cur, -1)).abs().to(torch.float32)
29
+ scores = scores + (-slopes * dist).to(scores.dtype)