From 4daab55ed54bfd81a8560f6dec2b2ccc0bd3e8dc Mon Sep 17 00:00:00 2001 From: liuzidi <71862493+liuzidi@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:30:42 +0800 Subject: [PATCH 1/3] Port SwiGLU forward and backward to VMI with ASC scheduling Take over PR #20, remove operator-local soft division, restore the ASC shell and document validation and remaining performance gaps. Co-authored-by: kuri780 --- extra-info/swiglu-vmi-takeover/README.md | 49 ++ extra-info/swiglu-vmi-takeover/bench_core.py | 102 ++++ extra-info/swiglu-vmi-takeover/check_shell.py | 28 + .../core-v5-performance.md | 54 ++ .../extended_backward.json | 496 ++++++++++++++++++ .../swiglu-vmi-takeover/extended_forward.json | 478 +++++++++++++++++ .../swiglu-vmi-takeover/toolchain/README.md | 1 + .../toolchain/atomic-build-audit.json | 10 + .../toolchain/ptoas-atomic-signedness.patch | 31 ++ .../toolchain/test_atomic_signedness.py | 26 + tile_kernels/quant/swiglu_backward_kernel.py | 4 +- tile_kernels/quant/swiglu_backward_vmi.py | 474 +++++++++++++++++ tile_kernels/quant/swiglu_forward_kernel.py | 4 +- tile_kernels/quant/swiglu_forward_vmi.py | 442 ++++++++++++++++ 14 files changed, 2195 insertions(+), 4 deletions(-) create mode 100644 extra-info/swiglu-vmi-takeover/README.md create mode 100644 extra-info/swiglu-vmi-takeover/bench_core.py create mode 100644 extra-info/swiglu-vmi-takeover/check_shell.py create mode 100644 extra-info/swiglu-vmi-takeover/core-v5-performance.md create mode 100644 extra-info/swiglu-vmi-takeover/extended_backward.json create mode 100644 extra-info/swiglu-vmi-takeover/extended_forward.json create mode 100644 extra-info/swiglu-vmi-takeover/toolchain/README.md create mode 100644 extra-info/swiglu-vmi-takeover/toolchain/atomic-build-audit.json create mode 100644 extra-info/swiglu-vmi-takeover/toolchain/ptoas-atomic-signedness.patch create mode 100644 extra-info/swiglu-vmi-takeover/toolchain/test_atomic_signedness.py create mode 100644 tile_kernels/quant/swiglu_backward_vmi.py create mode 100644 tile_kernels/quant/swiglu_forward_vmi.py diff --git a/extra-info/swiglu-vmi-takeover/README.md b/extra-info/swiglu-vmi-takeover/README.md new file mode 100644 index 00000000..942a8f80 --- /dev/null +++ b/extra-info/swiglu-vmi-takeover/README.md @@ -0,0 +1,49 @@ +# SwiGLU ASC → VMI takeover + +This work supersedes PTO-ISA/TileKernels-PTO#20, with provenance retained from kuri780's original VMI implementation. The replacement preserves the current ASC scheduling shell and routes the Ascend public API to the VMI factories. + +## Review disposition + +| Original feedback | Implementation / evidence | State | +|---|---|---| +| Omit optional masks (discussion_r4344151453) | Full masks omitted for operations whose frontend supports optional masks; required reduction/comparison/scalar-operation masks retained. | Implemented; full validation in progress | +| Restore differently typed T.copy using TileLang PR14 (discussion_r4344353367) | ASC DMA expressions restored verbatim. Latest pto-dev compiled and ran packed SF paths without applying PR14; PR14 itself remains open. No claim that it merged. | Representative packed paths pass; extended paths pending | +| Use a 256-lane amax register (discussion_r4344372026) | FP8 forward without clamp counts computes 256 lanes and reduces with group=8: eight outputs, each covering 32 channels. group=4 would combine 64 channels and violate sf_block=(1,32). Clamp-count path retains64. | hidden2048/3584 bit-exact, three rounds | +| Remove operator-local software exact divide | All local exact-div/sigmoid correction routines removed; precision='exact' requests the toolchain implementation. | Implemented; adversarial validation pending | +| Match ASC outside SIMD | Scalar predicates, buffers, DMA copies, pipeline stages, persistent loops and Simt atomic count code match ASC via normalized AST audit. | Audit passes; helper contracts also require manual review | + +## Frozen toolchain + +- TileLang PTO-ISA/pto-dev: `4eb83fdeb2fb317a0485052f51d0b77f4238d8b5`. +- PTOAS GitCode cann/master: `04c785491e844ec6d642e444a4e3ee84d1a60496`, plus a local Python-only atomic signedness fix. This is not unmodified master. The patch normalizes integer atomic pointer/value operands to signless common IR and restores the authored return type. +- CANN9.2.0; isolated builds, source and binary hashes recorded outside the repository pending final packaging. + +## Representative performance (not full acceptance) + +Same process, NPU1, ASC/VMI alternating order, three msprof rounds. Ratio=ASC_us/VMI_us; higher is faster. Weighted forward FP8 with packed column-major scales, no clamp: + +| hidden | ASC median us | VMI median us | median ratio | output/SF mismatch bytes | +|---|---|---|---|---| +| 2048 | 186.491 | 189.362 | 0.9848 | 0 | +| 3584 | 299.453 | 301.357 | 0.9937 | 0 | + +These measurements do not characterize backward, other scale layouts, clamp counts, or partial-width cases. Final performance tables and verified dependencies will replace this provisional report before merge. + +## Core matrix result, historical candidate v5 + +All24 forward and24 backward cases passed precision checks, each with3 alternating-order timing rounds. Median per-case ratios grouped by direction/dtype: + +| direction | BF16 | FP32 | FP8 | +|---|---|---|---| +| forward | 1.0333 | 1.0242 | 0.9719 | +| backward | 0.9030 | 0.9917 | 0.8771 | + +These group medians hide regressions: FP8 forward clamp at hidden2048 is0.8689. Experimental128-lane clamp compute/counters improves two gates to0.9471/0.9382 but has not completed full validation. The submitted source includes the divisor-aware logical-width fix for hidden not divisible by256; it matchesv5 on the core hidden2048/3584 configurations. The separate divisor-aware candidate has passed26 extended forward cases, including hidden128/384/4608 and all3 SF configurations with and without psum. All26 backward extended cases also pass. + +Backward artifacts show the BF16 memory path already matches ASC. One arithmetic difference is the reference's specialized single-neighbor sigmoid correction versus the general double-neighbor correction selected by VMI precision='exact'. A diagnostic ASC-only general-div substitution changes ratios toward0.95; this is causal-probe evidence, not a replacement acceptance baseline. Operator-local software divide remains removed. + +## Reproducing the audit + +`check_shell.py` compares the normalized ASC/VMI outer scheduling AST. `bench_core.py` runs the repository level-0 cases through the public API with original ASC and VMI factories, checks bytes (weight gradient uses the repository1e-4 tolerance), and records three alternating-order msprof rounds. It requires `SWIGLU_AUDIT_ROOT` pointing to an isolated directory with `tk`, `tilelang`, `ptoas`, `pysite/sitecustomize.py` and `ptoas-build.json` matching the exact commits above. `TILELANG_CACHE_DIR` must be fresh per revision, with matching PTODSL/native libraries and CANN environment. The host layout and pinned versions are asserted; the script is an investigation harness rather than a general installed-package benchmark. The public-API correctness tests against torch and final revision artifact/canary audit remain outstanding. + +The JSON case files describe the additional branch coverage; raw device logs and compiler dumps remain outside this PR to avoid committing large temporary artifacts. The per-case core table is historical candidatev5 and must be refreshed for the final source. diff --git a/extra-info/swiglu-vmi-takeover/bench_core.py b/extra-info/swiglu-vmi-takeover/bench_core.py new file mode 100644 index 00000000..02e4f8c3 --- /dev/null +++ b/extra-info/swiglu-vmi-takeover/bench_core.py @@ -0,0 +1,102 @@ +import argparse,importlib,importlib.util,json,os,sys,time,traceback,hashlib,subprocess +from pathlib import Path +import torch,torch_npu +import tile_kernels +from tilelang.profiler import do_bench +from tile_kernels.testing.bench import get_cast_params +from tile_kernels.quant import swiglu_forward_vmi as fv,swiglu_backward_vmi as bv +from tile_kernels.quant import swiglu_forward_asc as fa,swiglu_backward_asc as ba +p=argparse.ArgumentParser();p.add_argument('--direction',choices=['forward','backward'],required=True);p.add_argument('--only',default='');p.add_argument('--rounds',type=int,default=3);p.add_argument('--rep',type=int,default=20);p.add_argument('--out',required=True);p.add_argument('--correctness-only',action='store_true');p.add_argument('--tokens',type=int);a=p.parse_args() +torch.npu.set_device(int(os.environ.get('TK_DEVICE','1'))) +root=Path(os.environ['SWIGLU_AUDIT_ROOT']);out=Path(a.out);out.mkdir(parents=True,exist_ok=True) +expected={'tilelang':'4eb83fdeb2fb317a0485052f51d0b77f4238d8b5','ptoas':'04c785491e844ec6d642e444a4e3ee84d1a60496'} +sha=lambda x:hashlib.sha256(Path(x).read_bytes()).hexdigest() +import tilelang,ptoas,ptodsl +identity={} +for name,rev in expected.items(): + actual=subprocess.check_output(['git','-C',str(root/name),'rev-parse','HEAD'],text=True).strip();assert actual==rev + identity[name]=actual + identity[name+'_diff']=hashlib.sha256(subprocess.check_output(['git','-C',str(root/name),'diff','HEAD'])).hexdigest() +for mod in [fv,bv,fa,ba]: identity[mod.__name__]=sha(mod.__file__) +identity.update(tilelang_module=tilelang.__file__,ptodsl_module=ptodsl.__file__,ptoas_module=ptoas.__file__,manifest=sha(root/'ptoas-build.json'),atomic_python=sha(root/'ptoas/build/python/ptodsl/_ops_simt.py'),shim=sha(root/'pysite/sitecustomize.py')) +assert str(root/'tilelang') in tilelang.__file__;assert str(root/'ptoas') in ptodsl.__file__ +for folder in [out,Path(os.environ['TILELANG_CACHE_DIR'])]: + marker=folder/'revision_group.json' + if marker.exists(): assert json.loads(marker.read_text())==identity,'Revision mismatch' + else: marker.write_text(json.dumps(identity,indent=2)) +print(json.dumps(identity),flush=True) +spec=importlib.util.spec_from_file_location('test_swiglu',root/f'tk/tests/quant/test_swiglu_{a.direction}.py');test=importlib.util.module_from_spec(spec);spec.loader.exec_module(test) +params=test.generate_test_params(0);selected=set(map(int,a.only.split(','))) if a.only else set(range(len(params))) +if a.tokens: + for param in params:param['num_send_tokens']=a.tokens +vmodule=fv if a.direction=='forward' else bv;amodule=fa if a.direction=='forward' else ba +vname=f'get_swiglu_{a.direction}_kernel_vmi';aname=f'get_swiglu_{a.direction}_kernel_asc';vfactory=getattr(vmodule,vname);afactory=getattr(amodule,aname) +records=out/f'{a.direction}.jsonl';done=set() +if records.exists(): + for line in records.read_text().splitlines(): + try: + r=json.loads(line) + if r.get('status')=='pass':done.add(r['id']) + except ValueError:pass + +def compare(x,y,mask,path='out'): + if x is None: assert y is None;return [] + if isinstance(x,(tuple,list)): + assert len(x)==len(y);return sum([compare(xx,yy,mask,path+f'.{i}') for i,(xx,yy) in enumerate(zip(x,y))],[]) + assert x.shape==y.shape and x.dtype==y.dtype,(path,x.shape,y.shape) + if x.dtype==torch.float8_e4m3fn: x=x.view(torch.uint8);y=y.view(torch.uint8) + if x.ndim and x.shape[0]==mask.numel(): x=x[mask];y=y[mask] + xb=x.contiguous().view(torch.uint8);yb=y.contiguous().view(torch.uint8) + mism=int((xb!=yb).sum().item());maxabs=float((x.float()-y.float()).abs().nan_to_num().max().item()) if x.numel() else 0 + # weight_grad is the only repository tolerance exception. + weight=(a.direction=='backward' and params_current['with_weights'] and path==('out.2' if params_current['fmt']=='e4m3' else 'out.1')) + if weight: torch.testing.assert_close(x,y,atol=1e-4,rtol=1e-4) + else: assert mism==0,(path,mism,maxabs) + return [{'path':path,'mismatch_bytes':mism,'max_abs':maxabs,'tolerance':1e-4 if weight else 0}] +for idx,param in enumerate(params): + if idx not in selected or idx in done:continue + params_current=param + torch.manual_seed(1234+idx);torch.npu.manual_seed_all(1234+idx);tile_kernels.set_token_alignment(param['alignment']) + if 'num_sms' in param:tile_kernels.set_num_sms(param['num_sms']) + record={'id':idx,'params':param};print('START',a.direction,idx,param,flush=True) + try: + if a.direction=='forward': + x,mask,psum,w,c=test.generate_test_data(param) + def make(arm): + counts=c.clone() if param['clamp_value'] is not None else None + def call(): + setattr(vmodule,vname,vfactory if arm=='vmi' else afactory) + kw=dict(x=x,fmt=param['fmt'],psum_num_tokens_per_expert=psum,topk_weights=w if param['with_weights'] else None,routed_scaling_factor=param['routed_scaling_factor'],clamp_value=param['clamp_value'],clamped_count=counts) + if param['fmt']=='e4m3':return tile_kernels.quant.swiglu_forward_and_per_token_cast(**kw,num_per_channels=param['num_per_channels'],**get_cast_params(param)) + return tile_kernels.quant.swiglu_forward(**kw) + return call,counts + ac,acc=make('asc');vc,vcc=make('vmi') + else: + x,w,psum,grad=test.generate_test_data(param);mask=torch.arange(grad.shape[0],device=grad.device)