import json, sys, math, statistics as st from collections import defaultdict, Counter import matplotlib; matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np PATH="/home/admin/cpfs/wjh/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl" OUT="/tmp/wlc_out"; import os; os.makedirs(OUT, exist_ok=True) BLOCK=512 # --- transparent cost model for C3 (clearly-labeled estimate; raw-timing validation pending) --- PREFILL_TOK_S=7000.0 # MB1: 32k->4.5s ~7100 tok/s effective on H20 / 30B-A3B TPOT_S=0.010 # ~10ms/token decode (crossover unloaded ~5ms, loaded ~25ms) def pct(v,p): if not v: return float('nan') s=sorted(v);k=(len(s)-1)*p;f=int(k) return s[f] if f+1>=len(s) else s[f]+(s[f+1]-s[f])*(k-f) # ---------- Pass A: structure (scalars only) ---------- parents={}; recs={}; childcount=Counter() for line in open(PATH): if not line.strip(): continue d=json.loads(line); cid=d["chat_id"]; pid=d["parent_chat_id"] parents[cid]=pid recs[cid]=(float(d["timestamp"]),int(d["input_length"]),int(d["output_length"]),int(d["turn"])) if pid!="-1": childcount[pid]+=1 print(f"[A] records={len(recs)}", file=sys.stderr) root_of={} def root(cid): path=[];c=cid while True: if c in root_of:r=root_of[c];break p=parents.get(c,"-1") if p=="-1" or p not in recs:r=c;break path.append(c);c=p for x in path:root_of[x]=r root_of[cid]=r;return r sessions=defaultdict(list) for cid in recs: sessions[root(cid)].append(cid) seq={r:sorted(m,key=lambda c:(recs[c][3],recs[c][0])) for r,m in sessions.items()} print(f"[A] sessions={len(seq)}", file=sys.stderr) # ---------- C1: mixture + turn tail + hazard ---------- sr=mr=sm=mm=so=mo=0 turns_per=[] for r,s in seq.items(): multi=len(s)>1; turns_per.append(len(s)) for c in s: _,inl,outl,_=recs[c] if multi: mr+=1;mm+=inl;mo+=outl else: sr+=1;sm+=inl;so+=outl tot_r=sr+mr; tot_in=sm+mm; tot_out=so+mo cnt_turn=Counter() for r,s in seq.items(): for c in s: cnt_turn[recs[c][3]]+=1 hazard={k: (cnt_turn[k+1]/cnt_turn[k] if cnt_turn[k] else 0) for k in range(1,30)} # ---------- C2/C3: per-turn resident vs new-prefill (scalar) + hash_ids reuse ---------- by_in=defaultdict(list); by_new=defaultdict(list); by_out=defaultdict(list) by_reuse_hash=defaultdict(list) # hash-block prefix stability: reused/parent_blocks store={} # cid -> (blockset, in, out) for chats with pending children tot_new_prefill=0; tot_reused=0 for line in open(PATH): if not line.strip(): continue d=json.loads(line); cid=d["chat_id"]; pid=d["parent_chat_id"] inl=int(d["input_length"]); outl=int(d["output_length"]); turn=int(d["turn"]) blocks=set(d["hash_ids"]) if pid in store: pblk,pin,pout=store[pid] new_prefill=max(0, inl - pin - pout) # actual recompute (accounts for cached answer) reused_blk=len(blocks & pblk) by_reuse_hash[turn].append(reused_blk/len(pblk) if pblk else 0) childcount[pid]-=1 if childcount[pid]<=0: del store[pid] tot_reused += (inl-new_prefill) else: new_prefill=inl # session start: all new (intra-session) tot_new_prefill+=new_prefill by_in[turn].append(inl); by_new[turn].append(new_prefill); by_out[turn].append(outl) if childcount[cid]>0: store[cid]=(blocks,inl,outl) print(f"[B] done; store residual={len(store)}", file=sys.stderr) TURNS=[t for t in sorted(by_in) if len(by_in[t])>=50] med_in=[pct(by_in[t],.5) for t in TURNS] med_new=[max(pct(by_new[t],.5),1) for t in TURNS] med_out=[pct(by_out[t],.5) for t in TURNS] ratio=[med_in[i]/med_new[i] for i in range(len(TURNS))] reuse_pct=[(1-med_new[i]/med_in[i])*100 for i in range(len(TURNS))] # C3 time per turn (cost model) t_pref=[med_new[i]/PREFILL_TOK_S for i in range(len(TURNS))] t_dec=[med_out[i]*TPOT_S for i in range(len(TURNS))] # aggregate decode/prefill time fraction over a RANGE of constants def agg_time(prate,tpot): tp=tot_new_prefill/prate; td=tot_out*tpot; return td/(tp+td) frac_lo=agg_time(13000,0.005); frac_mid=agg_time(7000,0.010); frac_hi=agg_time(3000,0.025) chars={ "mixture":{"single_sessions":sr if False else sum(1 for s in seq.values() if len(s)==1), "multi_sessions":sum(1 for s in seq.values() if len(s)>1), "req_single_pct":sr/tot_r*100,"req_multi_pct":mr/tot_r*100, "in_single_pct":sm/tot_in*100,"in_multi_pct":mm/tot_in*100, "out_single_pct":so/tot_out*100,"out_multi_pct":mo/tot_out*100}, "turns":{"mean":st.mean(turns_per),"p99":pct(turns_per,.99),"max":max(turns_per), "single_turn_pct":sum(1 for x in turns_per if x==1)/len(turns_per)*100}, "hazard":hazard, "token_mass":{"total_input":tot_in,"total_output":tot_out,"out_in_ratio_pct":tot_out/tot_in*100, "new_prefill":tot_new_prefill,"reused_prefix":tot_reused, "new_prefill_pct_of_input":tot_new_prefill/tot_in*100}, "decode_time_fraction":{"optimistic_for_prefill":frac_lo,"mid":frac_mid,"pessimistic":frac_hi}, "per_turn":{"turn":TURNS,"med_resident_input":med_in,"med_new_prefill":med_new, "med_output":med_out,"resident_over_new":ratio,"reuse_pct":reuse_pct}, } json.dump(chars, open(f"{OUT}/chars.json","w"), indent=2) # ================= FIGURES ================= plt.rcParams.update({"figure.dpi":140,"font.size":10,"axes.grid":True,"grid.alpha":.3}) # ---- C1 ---- fig,ax=plt.subplots(1,3,figsize=(15,4.2)) cats=["% sessions","% requests","% input\ntokens","% output\ntokens"]; singv=[chars["mixture"]["single_sessions"]/len(seq)*100, chars["mixture"]["req_single_pct"], chars["mixture"]["in_single_pct"], chars["mixture"]["out_single_pct"]] multv=[100-x for x in singv] x=np.arange(len(cats)) ax[0].bar(x,singv,label="single-turn",color="#7fb3d5") ax[0].bar(x,multv,bottom=singv,label="multi-turn",color="#e74c3c") for i in range(len(cats)): ax[0].text(i,singv[i]/2,f"{singv[i]:.0f}",ha="center",va="center",fontsize=9) ax[0].text(i,singv[i]+multv[i]/2,f"{multv[i]:.0f}",ha="center",va="center",color="white",fontsize=9) ax[0].set_xticks(x);ax[0].set_xticklabels(cats);ax[0].set_ylabel("%");ax[0].set_ylim(0,100) ax[0].set_title("C1a Mixture: 90% sessions single-turn,\nbut multi-turn carries 2/3 prefill mass");ax[0].legend(loc="center right") # turn CCDF log-log tc=sorted(turns_per); n=len(tc); xs=sorted(set(tc)) ccdf=[sum(1 for v in tc if v>=xx)/n for xx in xs] ax[1].loglog(xs,ccdf,marker=".",ms=3,color="#34495e") ax[1].set_xlabel("turns per session (k)");ax[1].set_ylabel("P(turns >= k)") ax[1].set_title(f"C1b Heavy-tailed session length\n(p99={chars['turns']['p99']:.0f}, max={chars['turns']['max']})") # hazard hk=list(range(1,20)); hv=[hazard[k]*100 for k in hk] ax[2].plot(hk,hv,marker="o",color="#16a085") ax[2].set_xlabel("reached turn k");ax[2].set_ylabel("P(continue to k+1) %");ax[2].set_ylim(0,100) ax[2].set_title("C1c Continuation hazard rises 10%->94%\n(unpredictable at start, Lindy after)") fig.tight_layout(); fig.savefig(f"{OUT}/c1_session_mixture.png"); plt.close(fig) # ---- C2 ---- fig,ax=plt.subplots(1,3,figsize=(15,4.2)) ax[0].semilogy(TURNS,med_in,marker="o",label="resident context (input)",color="#e74c3c") ax[0].semilogy(TURNS,med_new,marker="s",label="new prefill this turn",color="#2980b9") ax[0].set_xlabel("turn");ax[0].set_ylabel("tokens (median, log)");ax[0].legend() ax[0].set_xlim(1,30) ax[0].set_title("C2a Resident state explodes,\nmarginal work collapses") ax[1].plot(TURNS,ratio,marker="o",color="#8e44ad") ax[1].set_xlabel("turn");ax[1].set_ylabel("resident / new-prefill");ax[1].set_xlim(1,30) ax[1].set_title("C2b The PD tax = resident/delta\n(grows to ~250x by deep turns)") ax[2].plot(TURNS,reuse_pct,marker="o",color="#27ae60") ax[2].set_xlabel("turn");ax[2].set_ylabel("per-turn reuse %");ax[2].set_ylim(50,100);ax[2].set_xlim(1,30) ax[2].set_title("C2c Per-turn reuse climbs to 99.6%\n(deep turns are near-pure cache hits)") fig.tight_layout(); fig.savefig(f"{OUT}/c2_work_amortization.png"); plt.close(fig) # ---- C3 ---- fig,ax=plt.subplots(1,2,figsize=(11,4.4)) # token mass decomposition vals=[tot_reused/1e9, tot_new_prefill/1e9, tot_out/1e9] labs=[f"reused prefix\n{tot_reused/tot_in*100:.0f}% of input", f"new prefill\n{tot_new_prefill/tot_in*100:.0f}% of input", f"decode output\n{tot_out/tot_in*100:.1f}% of input"] ax[0].bar(range(3),vals,color=["#95a5a6","#2980b9","#e67e22"]) ax[0].set_xticks(range(3));ax[0].set_xticklabels(labs,fontsize=8.5) ax[0].set_ylabel("tokens (billions)") ax[0].set_title("C3a Token mass: prefill-dominated\n(but tokens != time, see C3b)") # per-turn prefill vs decode TIME (cost model) ax[1].semilogy(TURNS,t_pref,marker="o",label="prefill time (new tok / 7k·s⁻¹)",color="#2980b9") ax[1].semilogy(TURNS,t_dec,marker="s",label="decode time (out·10ms)",color="#e67e22") ax[1].set_xlabel("turn");ax[1].set_ylabel("seconds (median, log)");ax[1].legend(fontsize=8);ax[1].set_xlim(1,30) ax[1].set_title(f"C3b Prefill→decode bottleneck flips within a session\n(agg decode-time share ≈ {frac_mid*100:.0f}%, range {frac_lo*100:.0f}–{frac_hi*100:.0f}%)") fig.tight_layout(); fig.savefig(f"{OUT}/c3_prefill_decode_balance.png"); plt.close(fig) print("FIGURES + chars.json written to", OUT) print(json.dumps({k:chars[k] for k in ["mixture","turns","token_mass","decode_time_fraction"]}, indent=2))