Marxist-Leninist commited on
Commit
82d098e
·
0 Parent(s):

AGILLM4-DiffusionBlocks: block-wise AR+SAT+NAT denoising, fused CE, tied heads

Browse files
Files changed (4) hide show
  1. README.md +42 -0
  2. dblocks_agillm4.py +82 -0
  3. dblocks_agillm4_lm.py +110 -0
  4. fused_ce.py +31 -0
README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AGILLM4-DiffusionBlocks
2
+
3
+ Block-wise (DiffusionBlocks-style) training adapted to the **AGILLM-4** decoder-only
4
+ LLM, with all three AGILLM-4 heads (AR + SAT fixed/variable + NAT) and a set of
5
+ long-context memory upgrades. Adapts & improves on
6
+ [SakanaAI/DiffusionBlocks](https://github.com/SakanaAI/DiffusionBlocks) (ICLR 2026),
7
+ whose released code is ViT/classification only.
8
+
9
+ ## What's here
10
+ - `dblocks_agillm4.py` — self-contained DiffusionBlocks prototype (proves the
11
+ per-step 1/B gradient locality on a small transformer).
12
+ - `dblocks_agillm4_lm.py` — real-architecture trainer: reuses AGILLM-4's `Encoder`
13
+ blocks (ALiBi, causal `Block`), partitions 28 layers into B EDM-noise blocks,
14
+ trains one block per step as an EDM denoiser, supervised by:
15
+ * **AR** — causal next-token CE
16
+ * **SAT** — fixed (proj CE over SAT_BLOCK, block-causal mask) **and** variable (gate CE)
17
+ * **NAT** — bidirectional mask-predict CE
18
+ Memory upgrades: EDM-weight clamp, AMP bf16, **tied shared vocab projection**
19
+ (1.21B -> 0.72B params), grad-checkpointed blocks, and `fused_ce`.
20
+ - `fused_ce.py` — fused cross-entropy that streams over the 129k vocab via
21
+ online-softmax with a custom backward, never materializing the `[T x 129280]`
22
+ logit matrix (the DiffusionBlocks "process in chunks" idea applied to the head).
23
+
24
+ ## Measured (real AGILLM-4 floor, 28L, 0.72B tied)
25
+ | ctx | full (28L) | one block (7L) |
26
+ |----:|-----------:|---------------:|
27
+ | 1280 | 7.48 GB | 5.53 GB |
28
+ | 4096 | 11.08 GB | 8.68 GB |
29
+ | 8192 | 22.11 GB | 19.37 GB |
30
+
31
+ ## Honest findings
32
+ - DiffusionBlocks and gradient-checkpointing are **substitutes** for activation
33
+ memory; with checkpointing on, the 28->7 layer saving is only ~1.1-1.3x.
34
+ - The big fixed-cost win was **tied heads + fused CE** (ctx-1280 ~24 GB -> ~5.5 GB).
35
+ - The long-context ceiling (16k+) is **attention-bound**, so the next lever is the
36
+ sublinear attention backend, not more block/CE work.
37
+
38
+ Status: validated research prototype. The official AGILLM-4 training line remains the
39
+ proven AR/SAT/NAT end-to-end trainer; these wins (tied heads, fused-CE, sublinear
40
+ attention) are intended to be folded into it for long context.
41
+
42
+ License: Apache-2.0 (matching the upstream method).
dblocks_agillm4.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DiffusionBlocks prototype for AGILLM-4 (block-wise denoising training).
2
+
3
+ Faithful to SakanaAI/DiffusionBlocks: EDM (Karras) diffusion, equi-probability
4
+ block sigma partitioning, and the key property -- each step trains ONE block as a
5
+ denoiser for its noise range, so backprop touches only that block => training
6
+ memory ~ 1/B of end-to-end. Here we reuse a transformer-block stack (stand-in for
7
+ AGILLM-4's Encoder.blocks: same pre-norm residual Block API x = blk(x, mask)).
8
+ """
9
+ import math, numpy as np, torch, torch.nn as nn, torch.nn.functional as F
10
+ def _cdf(x): return 0.5*(1+math.erf(x/math.sqrt(2)))
11
+ def _ppf(p): return float(torch.erfinv(torch.tensor(2*p-1.0))*math.sqrt(2))
12
+
13
+ # ---- EDM block partitioning (verbatim mechanism from dblock_modules.py) ----
14
+ def get_block_sigmas(num_blocks, sigma_min=0.002, sigma_max=80.0, p_mean=-1.2, p_std=1.2):
15
+ cdf_min = _cdf((np.log(sigma_min)-p_mean)/p_std)
16
+ cdf_max = _cdf((np.log(sigma_max)-p_mean)/p_std)
17
+ out=[]
18
+ for i in range(num_blocks+1):
19
+ p = cdf_min + (cdf_max-cdf_min)*(i/num_blocks)
20
+ out.append(float(np.exp(p_mean+p_std*_ppf(p))))
21
+ return out # descending->ascending sigma boundaries, equal CDF mass per block
22
+
23
+ class DBlockLM(nn.Module):
24
+ """Shared token emb + output proj; n_layers split into B independent blocks."""
25
+ def __init__(self, vocab=2048, d=256, n_layers=8, heads=4, num_blocks=4):
26
+ super().__init__()
27
+ self.emb = nn.Embedding(vocab, d)
28
+ self.blocks = nn.ModuleList([nn.TransformerEncoderLayer(d, heads, 4*d,
29
+ batch_first=True, norm_first=True) for _ in range(n_layers)])
30
+ self.ln = nn.LayerNorm(d); self.out = nn.Linear(d, vocab)
31
+ self.num_blocks = num_blocks
32
+ split = n_layers // num_blocks
33
+ self.assign = [list(range(i*split,(i+1)*split)) for i in range(num_blocks)]
34
+ self.block_sigmas = get_block_sigmas(num_blocks)
35
+ self.sigma_data = 0.5
36
+
37
+ def run_block(self, b, zt, sigma):
38
+ # EDM preconditioning (Karras 2022), exactly as DiffusionBlocks.denoise()
39
+ s = sigma[:,None,None]
40
+ c_skip = self.sigma_data**2/(s**2+self.sigma_data**2)
41
+ c_out = s*self.sigma_data/(s**2+self.sigma_data**2)**0.5
42
+ c_in = 1/(s**2+self.sigma_data**2)**0.5
43
+ h = zt*c_in
44
+ for li in self.assign[b]: # <-- ONLY this block's layers run
45
+ h = self.blocks[li](h)
46
+ return c_skip*zt + c_out*h # denoiser D_theta(zt,sigma) -> predicts z0
47
+
48
+ def edm_weight(sigma, sigma_data=0.5):
49
+ return (sigma**2+sigma_data**2)/(sigma*sigma_data)**2
50
+
51
+ def train_step(model, opt, ids):
52
+ z0 = F.normalize(model.emb(ids), p=2, dim=-1).detach() # clean target embeds
53
+ b = np.random.randint(model.num_blocks) # pick one block
54
+ lo, hi = model.block_sigmas[b], model.block_sigmas[b+1]
55
+ lo, hi = min(lo,hi), max(lo,hi)
56
+ sigma = torch.from_numpy(np.exp(np.random.uniform(np.log(lo),np.log(hi),
57
+ size=ids.shape[0])).astype('float32'))
58
+ zt = z0 + sigma[:,None,None]*torch.randn_like(z0)
59
+ D = model.run_block(b, zt, sigma) # backprop only block b
60
+ loss = (edm_weight(sigma)[:,None,None]*(D-z0)**2).mean()
61
+ opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
62
+ return b, loss.item()
63
+
64
+ if __name__ == "__main__":
65
+ torch.manual_seed(0)
66
+ NB=4; m=DBlockLM(num_blocks=NB); opt=torch.optim.AdamW(m.parameters(),1e-3)
67
+ ids=torch.randint(0,2048,(8,64))
68
+ print("layers:",len(m.blocks),"| blocks:",NB,"| layer assignment:",m.assign)
69
+ print("block sigma boundaries:",[round(s,3) for s in m.block_sigmas])
70
+ # one step, then verify ONLY the chosen block's layer params got gradients
71
+ b,loss=train_step(m,opt,ids)
72
+ def has_grad(mod): return any(p.grad is not None and p.grad.abs().sum()>0 for p in mod.parameters())
73
+ grad_blocks=[i for i in range(NB) if any(has_grad(m.blocks[li]) for li in m.assign[i])]
74
+ tot=sum(p.numel() for p in m.parameters())
75
+ blk_params=sum(p.numel() for li in m.assign[b] for p in m.blocks[li].parameters())
76
+ print(f"\\nstep trained block {b} loss={loss:.4f}")
77
+ print("blocks whose layers received gradients:",grad_blocks,"(expect just",[b],")")
78
+ print(f"layer-params with grad this step: {blk_params:,} / {sum(p.numel() for blk in m.blocks for p in blk.parameters()):,}"
79
+ f" ({100*blk_params/sum(p.numel() for blk in m.blocks for p in blk.parameters()):.0f}% = ~1/{NB})")
80
+ print("\\n--- a few more steps (each trains one block independently) ---")
81
+ for _ in range(6):
82
+ b,loss=train_step(m,opt,ids); print(f" block {b}: loss={loss:.4f}")
dblocks_agillm4_lm.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AR + SAT(fixed+variable) + NAT DiffusionBlocks for AGILLM-4 (v3, long-context).
2
+
3
+ Upgrades over v2:
4
+ * SAT fixed (proj CE over SAT_BLOCK, block-causal sat_mask) AND variable (gate CE)
5
+ * tied shared vocab projection (emb.weight) across AR/SAT/NAT -> drops ~0.5B params
6
+ * chunked cut-CE: logits computed per token-chunk on a detached hidden, backward
7
+ incrementally, then ONE block backward -> [T x 129280] never fully materialized
8
+ * sequential per-objective backward (one block-forward graph live at a time)
9
+ * AMP bf16 + clamped EDM weight (stable)
10
+ """
11
+ import sys, math, argparse, numpy as np, torch, torch.nn as nn, torch.nn.functional as F
12
+ import torch.utils.checkpoint as _ck
13
+ from fused_ce import fused_ce
14
+ sys.path.insert(0,"/workspace/agillm-4"); import nB300_agillm4 as M
15
+ V=M.VOCAB; SATB=M.SAT_BLOCK; EMIT=getattr(M,"EMIT_LAMBDA",0.1); SD=0.5
16
+ def _cdf(x): return 0.5*(1+math.erf(x/math.sqrt(2)))
17
+ def _ppf(p): return float(torch.erfinv(torch.tensor(2*p-1.0))*math.sqrt(2))
18
+ def bsig(B,smin=0.002,smax=80.0,pm=-1.2,ps=1.2):
19
+ a,b=_cdf((math.log(smin)-pm)/ps),_cdf((math.log(smax)-pm)/ps)
20
+ return [float(np.exp(pm+ps*_ppf(a+(b-a)*(i/B)))) for i in range(B+1)]
21
+ def edm_pre(s): s=s[:,None,None]; return SD**2/(s**2+SD**2), s*SD/(s**2+SD**2)**0.5, 1/(s**2+SD**2)**0.5
22
+ def edm_w(s,wmax=5.0): return float(((s**2+SD**2)/(s*SD)**2).clamp(max=wmax).mean())
23
+
24
+ def make(B):
25
+ cfg=M.PRESETS["agillm4_floor"].copy()
26
+ core=M.Encoder(cfg, attn_backend="sdpa", grad_checkpoint=False).to(M.DEV)
27
+ sat_gate=nn.Linear(cfg["d"],2).to(M.DEV) # SAT *variable* gate (tiny)
28
+ L=cfg["layers"]; sp=L//B
29
+ asg=[list(range(i*sp,(i+1)*sp)) for i in range(B)]; asg[-1]=list(range((B-1)*sp,L))
30
+ return core,sat_gate,asg,bsig(B)
31
+
32
+ def runblk(core,h,layers,mask):
33
+ for li in layers: h=_ck.checkpoint(core.blocks[li], h, mask, use_reentrant=False) # recompute in backward
34
+ return h
35
+
36
+ def cut_ce_backward(core, hidden, targets, scale, chunk=1024, mask_pos=None):
37
+ """chunked CE on detached hidden -> grad; caller backprops block once."""
38
+ hd=hidden.detach().requires_grad_(True); T=hd.size(1)
39
+ if mask_pos is None: N=targets.numel()
40
+ else: N=int(mask_pos.sum().item()) or 1
41
+ tot=0.0
42
+ for s in range(0,T,chunk):
43
+ lg=F.linear(hd[:,s:s+chunk], core.emb.weight).float() # tied proj, transient
44
+ tg=targets[:,s:s+chunk]
45
+ if mask_pos is None:
46
+ l=F.cross_entropy(lg.reshape(-1,V),tg.reshape(-1),reduction="sum")*scale/N
47
+ else:
48
+ mm=mask_pos[:,s:s+chunk]
49
+ if not bool(mm.any()): continue
50
+ l=F.cross_entropy(lg[mm],tg[mm],reduction="sum")*scale/N
51
+ l.backward(); tot+=float(l.detach())
52
+ return tot, hd.grad
53
+
54
+ def step(core,sat_gate,ids,layers,lo,hi,nat_ratio=0.5,bw=True):
55
+ T=ids.size(1); ar=sat=nat=0.0
56
+ sig=torch.from_numpy(np.exp(np.random.uniform(math.log(lo),math.log(hi),ids.size(0))).astype("float32")).to(M.DEV)
57
+ cs,co,ci=edm_pre(sig); w=edm_w(sig)
58
+ # ---- AR: causal diffusion denoise ----
59
+ with torch.autocast("cuda",dtype=torch.bfloat16):
60
+ emb=core.emb(ids); zt=emb+sig[:,None,None]*torch.randn_like(emb)
61
+ Dn=core.ln(cs*zt+co*runblk(core,ci*zt,layers,M.causal_mask(T)))
62
+ ar_loss=w*fused_ce(Dn[:,:-1].contiguous(), core.emb.weight, ids[:,1:].contiguous())
63
+ if bw: ar_loss.backward()
64
+ ar=float(ar_loss.detach())
65
+ # ---- SAT: block-causal diffusion; fixed proj + variable gate ----
66
+ with torch.autocast("cuda",dtype=torch.bfloat16):
67
+ zt2=core.emb(ids)+sig[:,None,None]*torch.randn_like(emb)
68
+ Ds=core.ln(cs*zt2+co*runblk(core,ci*zt2,layers,M.sat_mask(T)))
69
+ last=Ds[:,-SATB:]
70
+ satf=F.cross_entropy(F.linear(last,core.emb.weight).float().reshape(-1,V), ids[:,1:SATB+1].reshape(-1))
71
+ satv=EMIT*F.cross_entropy(sat_gate(Ds[:,0].float()), torch.ones(ids.size(0),dtype=torch.long,device=M.DEV))
72
+ sat_loss=w*(satf+satv)
73
+ if bw: sat_loss.backward()
74
+ sat=float(sat_loss)
75
+ # ---- NAT: bidirectional mask-predict ----
76
+ with torch.autocast("cuda",dtype=torch.bfloat16):
77
+ nat_ids=ids.clone(); m=torch.rand(ids.shape,device=M.DEV)<nat_ratio
78
+ if not bool(m.any()): m[...,-1]=True
79
+ nat_ids[m]=M.BLANK
80
+ Dnat=core.ln(runblk(core,core.emb(nat_ids),layers,None))
81
+ nat_loss=fused_ce(Dnat[m], core.emb.weight, ids[m])
82
+ if bw: nat_loss.backward()
83
+ nat=float(nat_loss.detach())
84
+ return ar,sat,nat
85
+
86
+ def peak(fn):
87
+ torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats(); fn(); torch.cuda.synchronize()
88
+ return torch.cuda.max_memory_allocated()/1e9
89
+
90
+ if __name__=="__main__":
91
+ ap=argparse.ArgumentParser(); ap.add_argument("--blocks",type=int,default=4); a=ap.parse_args()
92
+ core,sg,asg,bs=make(a.blocks); nL=len(core.blocks); bi=a.blocks//2
93
+ P=sum(p.numel() for p in core.parameters())+sum(p.numel() for p in sg.parameters())
94
+ print(f"AGILLM-4 floor {nL}L, {P/1e9:.2f}B params (tied heads) | blocks={a.blocks} assign={asg}")
95
+ def full(ctx): step(core,sg,torch.randint(0,V,(1,ctx),device=M.DEV),list(range(nL)),bs[0],bs[-1]); core.zero_grad(set_to_none=True); sg.zero_grad(set_to_none=True)
96
+ def blk(ctx): step(core,sg,torch.randint(0,V,(1,ctx),device=M.DEV),asg[bi],bs[bi],bs[bi+1]); core.zero_grad(set_to_none=True); sg.zero_grad(set_to_none=True)
97
+ print("\\n=== v3: tied-heads + cut-CE + AMP + SAT(fixed+var)+NAT ===")
98
+ for ctx in (1280,4096,8192,16384):
99
+ try: mf=peak(lambda:full(ctx)); sf=f"{mf:.2f} GB"
100
+ except RuntimeError as e: torch.cuda.empty_cache(); sf=("OOM" if "out of memory" in str(e).lower() else "ERR")
101
+ try: mb=peak(lambda:blk(ctx)); sb=f"{mb:.2f} GB"
102
+ except RuntimeError as e: torch.cuda.empty_cache(); sb=("OOM" if "out of memory" in str(e).lower() else "ERR")
103
+ print(f" ctx={ctx:>6}: full(28L)={sf:>9} one block({len(asg[bi])}L)={sb:>9}")
104
+ print("\\n--- smoke train (block-wise AR+SAT(fixed+var)+NAT) ---")
105
+ for st in range(4):
106
+ b=np.random.randint(a.blocks)
107
+ ar,sat,nat=step(core,sg,torch.randint(0,V,(1,1280),device=M.DEV),asg[b],bs[b],bs[b+1])
108
+ opt=torch.optim.SGD([p for li in asg[b] for p in core.blocks[li].parameters()]+list(sg.parameters()),1e-3)
109
+ opt.step(); opt.zero_grad(); core.zero_grad(set_to_none=True)
110
+ print(f" step {st} block {b}: AR={ar:.2f} SAT={sat:.2f} NAT={nat:.2f}")
fused_ce.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fused cross-entropy: streams over the VOCAB dimension (online-softmax) so the
2
+ [N x V] logit matrix is NEVER materialized -- only [N x vchunk]. Custom backward
3
+ recomputes softmax per vocab-chunk (grad = softmax - onehot). This is the
4
+ DiffusionBlocks 'process in chunks, don't hold the whole thing' idea applied to
5
+ the output head instead of network depth."""
6
+ import torch
7
+ class FusedCE(torch.autograd.Function):
8
+ @staticmethod
9
+ def forward(ctx, h, W, tgt, vchunk=16384):
10
+ N,d=h.shape; V=W.shape[0]; hf=h.float()
11
+ m=torch.full((N,),-1e30,device=h.device); s=torch.zeros(N,device=h.device); zt=torch.zeros(N,device=h.device)
12
+ for c in range(0,V,vchunk):
13
+ lg=hf@W[c:c+vchunk].float().T # [N,vchunk] transient only
14
+ cm=lg.max(1).values; nm=torch.maximum(m,cm)
15
+ s=s*torch.exp(m-nm)+torch.exp(lg-nm[:,None]).sum(1); m=nm
16
+ ic=(tgt>=c)&(tgt<c+vchunk)
17
+ if ic.any(): zt[ic]=lg[ic,tgt[ic]-c]
18
+ lse=m+torch.log(s); ctx.save_for_backward(h,W,tgt,lse); ctx.vchunk=vchunk
19
+ return (lse-zt).mean()
20
+ @staticmethod
21
+ def backward(ctx, go):
22
+ h,W,tgt,lse=ctx.saved_tensors; vc=ctx.vchunk; N,d=h.shape; V=W.shape[0]; hf=h.float()
23
+ gh=torch.zeros_like(hf); gW=torch.zeros(W.shape,device=W.device,dtype=torch.float32); sc=float(go)/N
24
+ for c in range(0,V,vc):
25
+ Wc=W[c:c+vc].float(); p=torch.exp(hf@Wc.T-lse[:,None]) # softmax chunk [N,vchunk]
26
+ ic=(tgt>=c)&(tgt<c+vc)
27
+ if ic.any(): p[ic,tgt[ic]-c]-=1.0
28
+ p*=sc; gh+=p@Wc; gW[c:c+vc]+=p.T@hf
29
+ return gh.to(h.dtype), gW.to(W.dtype), None, None
30
+ def fused_ce(h, W, tgt, vchunk=16384):
31
+ return FusedCE.apply(h.reshape(-1,h.size(-1)), W, tgt.reshape(-1), vchunk)