Skip to content

Port SwiGLU forward and backward to VMI with ASC scheduling - #29

Draft
liuzidi wants to merge 3 commits into
PTO-ISA:vmi-implfrom
liuzidi:feat/swiglu-vmi-takeover
Draft

liuzidi wants to merge 3 commits into
PTO-ISA:vmi-implfrom
liuzidi:feat/swiglu-vmi-takeover

Conversation

@liuzidi

@liuzidi liuzidi commented Oct 26, 2026 •

Copy link
Copy Markdown

Port SwiGLU forward and backward to VMI while preserving the ASC scheduling shell, DMA copies, buffer geometry, scalar predicates, and synchronization. The Ascend public API now selects the VMI factories. This takes over #20, retaining provenance from kuri780's implementation.

Operator-local software division and sigmoid correction routines are removed. Exact division delegates to V.vdiv(..., precision='exact'). Optional full masks are omitted where supported. Forward grouped amax uses group=8 for 256 lanes to retain SF32 semantics; group=4 would incorrectly combine64 channels.

This is a draft pending final correctness/revision validation. Performance below the original0.95 target is accepted by the task owner and is documented rather than used as a merge gate. Historical candidatev5 passed24 forward and24 backward core configurations with three paired timing rounds. Extended forward26 and backward26 cases pass; finite-boundary forward7 cases also pass. Remaining checks are final submitted-source correctness and toolchain dependency verification. The final source will be revalidated before merge.

Historical median ASC/VMI ratios: forward BF16/FP32/FP8=1.033/1.024/0.972; backward=0.903/0.992/0.877. These medians hide individual gaps, particularly forwardFP8 clamp. Detailed historical per-case measurements are listed below. The submitted source includes the partial-width fix validated by the extended matrix. Clamp optimization remains experimental and is not included; historical numbers must not be attributed to a different source revision.

Toolchain: TileLang pto-dev 4eb83fdeb2fb317a0485052f51d0b77f4238d8b5, GitCode cann/pto-as master 04c785491e844ec6d642e444a4e3ee84d1a60496, CANN9.2.0. PTOAS also requires a local Python atomic signedness patch for unchanged ASC-style clamp-count atomics. This dependency is not unmodified upstream master and must be resolved/reproduced before merge.

Original review disposition: optional masks omitted where supported; ASC DMA expressions restored; 256-lane SF32 reduction uses group=8. TileLang PR #14 remains open; the restored copies compile on the pinned pto-dev. ASC reference files remain unchanged. Investigation scripts, logs and compiler patches are retained locally rather than added to the operator diff.

Final submitted-source validation is running on both NPUs: repository torch-reference tests ondevice1, and a fresh48-case paired ASC/VMI three-round sweep ondevice0. Unmerged clamp performance experiments are excluded from this PR.

The authenticated author liuzidi can publish this fork PR but currently has no upstream push/merge permission (GitHub repository permissions API). A maintainer will need to execute the eventual merge after the correctness and dependency checks complete.

Historical v5 per-case performance (ASC/VMI; not final-source results)

Candidate v5 full level-0 performance

ASC/VMI ratio; three alternating-order msprof rounds. All24 forward and24 backward configs passed; this remains a historical candidate, not final acceptance.

direction id dtype hidden clamp weighted ASC us VMI us ratio
forward 0 bf16 2048 None True 185.870 184.815 1.0057
forward 1 bf16 2048 0.5 False 263.230 249.013 1.0572
forward 2 bf16 3584 None True 307.459 304.546 1.0094
forward 3 bf16 3584 0.5 False 416.697 385.308 1.0815
forward 4 fp32 2048 None True 186.743 185.448 1.0055
forward 5 fp32 2048 0.5 False 261.396 250.546 1.0429
forward 6 fp32 3584 None True 295.041 295.434 0.9987
forward 7 fp32 3584 0.5 False 406.328 386.594 1.0510
forward 8 e4m3 2048 None True 188.229 190.086 0.9906
forward 9 e4m3 2048 0.5 False 236.488 272.169 0.8689
forward 10 e4m3 3584 None True 301.562 300.561 1.0033
forward 11 e4m3 3584 0.5 False 343.710 360.890 0.9523
forward 12 bf16 2048 None True 241.353 240.101 1.0049
forward 13 bf16 2048 0.5 False 336.385 314.447 1.0698
forward 14 bf16 3584 None True 403.246 401.854 1.0034
forward 15 bf16 3584 0.5 False 535.617 496.265 1.0792
forward 16 fp32 2048 None True 240.599 243.239 0.9900
forward 17 fp32 2048 0.5 False 328.744 311.167 1.0565
forward 18 fp32 3584 None True 391.921 391.033 1.0023
forward 19 fp32 3584 0.5 False 521.857 494.930 1.0543
forward 20 e4m3 2048 None True 232.879 235.517 0.9887
forward 21 e4m3 2048 0.5 False 299.903 345.063 0.8694
forward 22 e4m3 3584 None True 374.345 375.767 0.9962
forward 23 e4m3 3584 0.5 False 441.645 462.494 0.9551
backward 0 e4m3 2048 None True 615.716 708.856 0.8685
backward 1 e4m3 2048 0.5 False 491.104 546.207 0.8991
backward 2 e4m3 3584 None True 1007.959 1166.023 0.8645
backward 3 e4m3 3584 0.5 False 824.452 930.955 0.8856
backward 4 bf16 2048 None True 444.013 491.066 0.9043
backward 5 bf16 2048 0.5 False 312.723 348.405 0.8976
backward 6 bf16 3584 None True 731.402 808.218 0.9050
backward 7 bf16 3584 0.5 False 515.387 595.606 0.8653
backward 8 fp32 2048 None True 516.023 530.469 0.9732
backward 9 fp32 2048 0.5 False 439.859 443.067 0.9933
backward 10 fp32 3584 None True 817.543 823.705 0.9915
backward 11 fp32 3584 0.5 False 654.139 659.459 0.9922
backward 12 e4m3 2048 None True 679.313 784.374 0.8660
backward 13 e4m3 2048 0.5 False 542.913 603.417 0.8998
backward 14 e4m3 3584 None True 1119.164 1295.341 0.8640
backward 15 e4m3 3584 0.5 False 883.174 997.082 0.8858
backward 16 bf16 2048 None True 468.032 517.696 0.9042
backward 17 bf16 2048 0.5 False 345.110 382.791 0.9018
backward 18 bf16 3584 None True 807.723 892.651 0.9049
backward 19 bf16 3584 0.5 False 552.426 638.133 0.8657
backward 20 fp32 2048 None True 556.825 572.693 0.9722
backward 21 fp32 2048 0.5 False 475.048 478.310 0.9931
backward 22 fp32 3584 None True 875.469 882.675 0.9919
backward 23 fp32 3584 0.5 False 723.930 730.733 0.9907

liuzidi and others added 3 commits October 26, 2026 16:30
Take over PR PTO-ISA#20, remove operator-local soft division, restore the ASC shell and document validation and remaining performance gaps.

Co-authored-by: kuri780 <kuri780@users.noreply.github.com>
@liuzidi

liuzidi commented Oct 26, 2026

Copy link
Copy Markdown
Author

Forward:ASC → VMI 完整源码 diff

对应 PR head 67f68202e235f03f10268872a69f4dd9b559d22a。直接比较同一提交中的 swiglu_forward_asc.py 与 swiglu_forward_vmi.py,未归一化名称、未隐藏 helper/注释/空白差异;标准 unified diff,省略的只有未变化行。ASC 文件本身没有修改。

--- a/tile_kernels/quant/swiglu_forward_asc.py
+++ b/tile_kernels/quant/swiglu_forward_vmi.py
@@ -3,13 +3,14 @@
 import tilelang
 from tilelang.ascend import language as T
 from tilelang.ascend.language import simd as S
+from tilelang.ascend.language import vmi as V
 
 from tile_kernels.quant.common import CastOutputConfig, get_packed_ue8m0_pack_factor, get_sf_shape
 from tile_kernels.utils import align
 
 
-@tilelang.jit
-def get_swiglu_forward_kernel_asc(
+@tilelang.jit(target='pto')
+def get_swiglu_forward_kernel_vmi(
     hidden: int,
     num_experts: int,
     alignment: int,
@@ -54,24 +55,38 @@
 
     @T.macro
     def get_scale_and_inverse(amax):
-        clamped_amax = S.vmaxs(amax, out_config.clamp_min_value)
-        scale_bits = T.reinterpret(S.vmuls(clamped_amax, 1.0 / 448.0), 'uint32x64')
-        exponent = S.vadds(S.vshrs(S.vsub(scale_bits, S.vdup(1, T.uint32)), 23), 1)
-        inverse = T.reinterpret(S.vshls(S.vsub(S.vdup(254, T.uint32), exponent), 23), 'float32x64')
+        full = V.create_mask(64, size=64)
+        clamped_amax = V.vmaxs(amax, T.float32(out_config.clamp_min_value), full)
+        scale_bits = V.vinterpret_cast(V.vmuls(clamped_amax, T.float32(1.0 / 448.0), full), 'uint32')
+        exponent = V.vadds(
+            V.vshrs(V.vsub(scale_bits, V.vbrc(T.uint32(1), size=64)), T.uint32(23), full),
+            T.uint32(1),
+            full,
+        )
+        inverse = V.vinterpret_cast(
+            V.vshls(V.vsub(V.vbrc(T.uint32(254), size=64), exponent), T.uint32(23), full),
+            'float32',
+        )
         if out_config.use_packed_ue8m0:
             return exponent, inverse
-        return T.reinterpret(S.vshls(exponent, 23), 'float32x64'), inverse
+        return V.vinterpret_cast(V.vshls(exponent, T.uint32(23), full), 'float32'), inverse
 
     @T.macro
     def init_transpose_indices(indices):
         # Precompute gather indices shared by every token group when converting the
         # dense token-major scale buffer to the packed col-major output layout.
         with T.SimdVF():
-            lane = S.vci(0, T.int16)
-            output_row = S.vand(T.reinterpret(lane, 'uint16x128'), S.vdup(token_group - 1, T.uint16))
-            group_in_vector = T.reinterpret(S.vshrs(lane, int(math.log2(token_group))), 'uint16x128')
-            source_indices = S.vadd(S.vmuls(output_row, align(num_groups, 64) // pack_factor), group_in_vector)
-            S.vsts(indices[0], source_indices, dist='NORM_B16', extent=128)
+            lane = V.vci(T.int16(0), size=128)
+            output_row = V.vand(V.vinterpret_cast(lane, 'uint16'), V.vbrc(T.uint16(token_group - 1), size=128))
+            group_in_vector = V.vinterpret_cast(
+                V.vshr(lane, V.vbrc(T.int16(int(math.log2(token_group))), size=128)),
+                'uint16',
+            )
+            source_indices = V.vadd(
+                V.vmul(output_row, V.vbrc(T.uint16(align(num_groups, 64) // pack_factor), size=128)),
+                group_in_vector,
+            )
+            V.vstore(source_indices, indices[0])
 
     @T.macro
     def transpose_packed_scales(src, dst, indices):
@@ -81,134 +96,166 @@
             num_output_values = num_groups // pack_factor * token_group
             num_full_vectors = num_output_values // 128
             num_tail_values = num_output_values % 128
-            source_indices = S.vld(indices[0])
-            src_u16 = T.Tensor((token_group, align(num_groups, 64) // pack_factor), T.uint16, src.data)
+            source_indices = V.vload(indices[0], size=128)
+            full_mask = V.create_mask(128, size=128)
+            # Read byte-owned SF storage without introducing a typed pointer alias.
+            byte_indices = V.vmul(source_indices, V.vbrc(T.uint16(2), size=128))
 
             if num_full_vectors > 0:
                 for output_vector in T.unroll(num_full_vectors, explicit=True):
                     group_base = output_vector * groups_per_vector
-                    values = S.vgather2(src_u16[0, group_base], source_indices)
-                    S.vsts(dst[group_base, 0], values, dist='NORM_B16', extent=128)
+                    low = V.vgather(src[0, group_base * 2], byte_indices, full_mask)
+                    high = V.vgather(src[0, group_base * 2 + 1], byte_indices, full_mask)
+                    values = V.vor(low, V.vshls(high, T.uint16(8), full_mask))
+                    V.vstore(values, dst[group_base, 0])
 
             if num_tail_values > 0:
                 tail_group_base = num_full_vectors * groups_per_vector
-                tail_mask = S.pset(16, f'PAT_VL{num_tail_values}')
-                values = S.vgather2(src_u16[0, tail_group_base], source_indices, tail_mask)
-                S.vsts(dst[tail_group_base, 0], values, tail_mask, dist='NORM_B16', extent=num_tail_values)
+                tail_mask = V.create_mask(num_tail_values, size=128)
+                low = V.vgather(src[0, tail_group_base * 2], byte_indices, tail_mask)
+                high = V.vgather(src[0, tail_group_base * 2 + 1], byte_indices, tail_mask)
+                values = V.vor(low, V.vshls(high, T.uint16(8), tail_mask))
+                V.vstore(values, dst[tail_group_base, 0], tail_mask)
 
     @T.macro
     def compute_swiglu(xl_ub, xr_ub, values_ub, out_ub, weight, precision, amax_ub, with_certificate, pacc_buf, count_clamp, acc=None):
         # Compute SiLU(x_l) * x_r, optionally collecting clamp counters,
         # quantization amax values, and the fast-division safety certificate.
-        zeros = S.vdup(0.0, T.float32)
+        # Logical compute width is local to SIMD; ASC buffers and scheduler
+        # retain their original 64-lane geometry and certificate storage.
+        logical_size = (256 if hidden % 256 == 0 else 128) if out_config.with_sf and not count_clamp else 64
+        logical_vectors = hidden // logical_size
+        compute_unroll = 1 if logical_size > 64 else 4
+        full = V.create_mask(logical_size, size=logical_size)
+        zeros = V.vbrc(T.float32(0.0), size=logical_size)
         if count_clamp:
             if acc is None:
-                acc = S.alloc_local((3,), T.float32)
+                acc = V.alloc_local((3,), V.vreg(logical_size, T.float32))
                 for i in T.unroll(3, explicit=True):
                     acc[i] = zeros
         if with_certificate:
-            certificate_min = S.alloc_var(T.uint32)
-            certificate_max = S.alloc_var(T.uint32)
-            certificate_min = S.vdup(0xFFFFFFFF, T.uint32)
-            certificate_max = S.vdup(0, T.uint32)
+            # Loop-carried across tile_id: alloc_var is what lets the eager
+            # builder rebind these accumulators inside the loop.
+            certificate_min = V.alloc_var(T.uint32, size=logical_size)
+            certificate_max = V.alloc_var(T.uint32, size=logical_size)
+            certificate_min = V.vbrc(T.uint32(0xFFFFFFFF), size=logical_size)
+            certificate_max = V.vbrc(T.uint32(0), size=logical_size)
         else:
             certificate_min = None
             certificate_max = None
-        if out_config.with_sf:
-            amax_store_mask = S.pset(32, 'PAT_VL8')
-            amax_acc = S.alloc_local((4,), T.float32)
-            for i in T.unroll(4, explicit=True):
-                amax_acc[i] = S.vdup(0.0, T.float32)
-
-        for tile_id in T.serial(T.ceildiv(num_vecs, 4)):
-            for sub in T.unroll(4, explicit=True):
-                vector_id = tile_id * 4 + sub
-                if vector_id < num_vecs:
-                    col = vector_id * vec_size
+        for tile_id in T.serial(T.ceildiv(logical_vectors, compute_unroll)):
+            for sub in T.unroll(compute_unroll, explicit=True):
+                vector_id = tile_id * compute_unroll + sub
+                if vector_id < logical_vectors:
+                    col = vector_id * logical_size
                     if in_dtype == T.bfloat16:
-                        val_l = S.vcvt(S.vld(xl_ub[col], dist='UNPK_B16'), T.float32, part=0)
-                        val_r = S.vcvt(S.vld(xr_ub[col], dist='UNPK_B16'), T.float32, part=0)
+                        val_l = V.vcvt(V.vload(xl_ub[col], size=logical_size), 'float32')
+                        val_r = V.vcvt(V.vload(xr_ub[col], size=logical_size), 'float32')
                     else:
-                        val_l = S.vld(xl_ub[col])
-                        val_r = S.vld(xr_ub[col])
+                        val_l = V.vload(xl_ub[col], size=logical_size)
+                        val_r = V.vload(xr_ub[col], size=logical_size)
                     if has_clamp:
-                        silu_l = S.vmins(val_l, clamp_value)
-                        mul_r = S.vmaxs(S.vmins(val_r, clamp_value), -clamp_value)
+                        silu_l = V.vmins(val_l, T.float32(clamp_value), full)
+                        mul_r = V.vmaxs(V.vmins(val_r, T.float32(clamp_value), full), T.float32(-clamp_value), full)
                     else:
                         silu_l = val_l
                         mul_r = val_r
-                    denominator = S.vadds(S.vexpdif(zeros, silu_l), 1.0)
-                    quotient = S.vdiv(silu_l, denominator, precision=precision)
-                    val = S.vmul(quotient, mul_r)
-                    out_val = S.vmuls(val, weight) if with_weight else val
+                    denominator = V.vadds(V.vexpdif(zeros, silu_l, full), T.float32(1.0), full)
+                    if precision == 'ftz_true':
+                        quotient = V.vdiv(silu_l, denominator)
+                    elif precision == 'exact':
+                        quotient = V.vdiv(silu_l, denominator, full, precision='exact')
+                    else:
+                        raise ValueError(f"unsupported vdiv precision {precision!r}; expected 'ftz_true' or 'exact'")
+                    val = V.vmul(quotient, mul_r)
+                    out_val = V.vmuls(val, weight, full) if with_weight else val
 
                     if out_config.with_sf:
-                        S.vsts(values_ub[col], out_val)
+                        V.vstore(out_val, values_ub[col])
                     elif out_config.dtype == T.bfloat16:
-                        S.vsts(out_ub[col], S.vcvt(out_val, T.bfloat16), dist='PK_B32')
+                        V.vstore(V.vcvt(out_val, 'bfloat16', saturate='NOSAT'), out_ub[col])
                     else:
-                        S.vsts(out_ub[col], out_val)
+                        V.vstore(out_val, out_ub[col])
 
                     if with_certificate:
                         # Keep the low 19 mantissa bits; values too close to either end of this window use the exact fallback.
-                        metric = S.vand(T.reinterpret(out_val, 'uint32x64'), S.vdup(0x7FFFF, T.uint32))
-                        certificate_min = S.vmin(certificate_min, metric)
-                        certificate_max = S.vmax(certificate_max, metric)
+                        metric = V.vand(V.vinterpret_cast(out_val, 'uint32'), V.vbrc(T.uint32(0x7FFFF), size=logical_size))
+                        certificate_min = V.vmin(certificate_min, metric)
+                        certificate_max = V.vmax(certificate_max, metric)
                     if count_clamp:
-                        clamped_mask_0 = S.vcmps(val_l, clamp_value, op='gt')
-                        clamped_mask_1 = S.vcmps(val_r, clamp_value, op='gt')
-                        clamped_mask_2 = S.vcmps(val_r, -clamp_value, op='lt')
-                        acc[0] = S.vadds(acc[0], 1.0, clamped_mask_0, mode='MODE_MERGING')
-                        acc[1] = S.vadds(acc[1], 1.0, clamped_mask_1, mode='MODE_MERGING')
-                        acc[2] = S.vadds(acc[2], 1.0, clamped_mask_2, mode='MODE_MERGING')
+                        clamped_mask_0 = V.vcmps(val_l, T.float32(clamp_value), full, 'gt')
+                        clamped_mask_1 = V.vcmps(val_r, T.float32(clamp_value), full, 'gt')
+                        clamped_mask_2 = V.vcmps(val_r, T.float32(-clamp_value), full, 'lt')
+                        # Merge-mode accumulation: add 1 on active lanes, keep the
+                        # previous value elsewhere (PTO vadds only supports zeroing).
+                        one = V.vbrc(T.float32(1.0), size=logical_size)
+                        acc[0] = V.vadd(acc[0], V.vsel(clamped_mask_0, one, zeros))
+                        acc[1] = V.vadd(acc[1], V.vsel(clamped_mask_1, one, zeros))
+                        acc[2] = V.vadd(acc[2], V.vsel(clamped_mask_2, one, zeros))
                     if out_config.with_sf:
-                        amax_acc[sub] = S.vabs(out_val)
-            if out_config.with_sf:
-                # 256-element tile amax reduction
-                low01, high01 = S.vdintlv(amax_acc[0], amax_acc[1])
-                low23, high23 = S.vdintlv(amax_acc[2], amax_acc[3])
-                compressed01 = S.vmax(low01, high01)
-                compressed23 = S.vmax(low23, high23)
-                low, high = S.vdintlv(compressed01, compressed23)
-                amax = S.vcgmax(S.vmax(low, high))
-                S.vsts(amax_ub[tile_id * 8], amax, amax_store_mask, dist='NORM_B32', extent=8)
+                        amax = V.vcmax(V.vabs(out_val), full, group=logical_size // 32)
+                        V.vstore(amax, amax_ub[vector_id * (logical_size // 32)])
         if count_clamp:
             for i in T.unroll(3, explicit=True):
-                S.vsts(pacc_buf[i, 0], S.vadd(S.vld(pacc_buf[i, 0]), acc[i]))
-        return certificate_min, certificate_max
+                V.vstore(V.vadd(V.vload(pacc_buf[i, 0], size=logical_size), acc[i]), pacc_buf[i, 0])
+        if with_certificate and logical_size != 64:
+            # The public helper contract remains two 64-lane values, allowing
+            # ASC's outer certificate buffers and scalar fallback to stay exact.
+            certificate_min_out = V.vbrc(V.vcmin(certificate_min, full), size=64, group=1)
+            certificate_max_out = V.vbrc(V.vcmax(certificate_max, full), size=64, group=1)
+        else:
+            certificate_min_out = certificate_min
+            certificate_max_out = certificate_max
+        return certificate_min_out, certificate_max_out
 
     @T.macro
     def quantize_f32(values_ub, amax_ub, inverse_ub, sf_dense_ub, out_ub, token_offset):
         # Get inverse scale and quantize FP32 SwiGLU result to FP8.
         with T.SimdVF():
+            full = V.create_mask(vec_size, size=vec_size)
+            full_scale = V.create_mask(64, size=64)
             for group_batch in T.serial(T.ceildiv(num_groups, 64)):
                 group = group_batch * 64
-                amax = S.vld(amax_ub[group])
+                amax = V.vload(amax_ub[group], size=64)
                 if out_config.round_sf:
                     scales_or_exponents, inverses = get_scale_and_inverse(amax)
                     if out_config.use_packed_ue8m0:
-                        S.vsts(sf_dense_ub[token_offset, group], T.reinterpret(scales_or_exponents, 'uint8x256'), dist='PK4_B32')
+                        # int-narrow (uint32 -> uint8): NOSAT is exactly ASC's
+                        # reinterpret-truncation, unrelated to the FP8 output
+                        # saturate decision below. Spell it out because PTODSL
+                        # otherwise defaults to SAT for narrowing, which differs
+                        # for exponents >= 256 (amax = inf).
+                        V.vstore(V.vcvt(scales_or_exponents, 'uint8', saturate='NOSAT'), sf_dense_ub[token_offset, group])
                     else:
-                        S.vsts(sf_dense_ub[token_offset, group], scales_or_exponents)
-                    S.vsts(inverse_ub[group], inverses)
+                        V.vstore(scales_or_exponents, sf_dense_ub[token_offset, group])
+                    V.vstore(inverses, inverse_ub[group])
                 else:
-                    clamped_amax = S.vmaxs(amax, out_config.clamp_min_value)
-                    quant_max = S.vdup(448.0, T.float32)
-                    S.vsts(sf_dense_ub[token_offset, group], S.vdiv(clamped_amax, quant_max))
-                    S.vsts(inverse_ub[group], S.vdiv(quant_max, clamped_amax))
+                    clamped_amax = V.vmaxs(amax, T.float32(out_config.clamp_min_value), full_scale)
+                    quant_max = V.vbrc(T.float32(448.0), size=64)
+                    V.vstore(V.vdiv(clamped_amax, quant_max, full_scale, precision='exact'), sf_dense_ub[token_offset, group])
+                    V.vstore(V.vdiv(quant_max, clamped_amax, full_scale, precision='exact'), inverse_ub[group])
 
             S.mem_bar('VST_VLD')
-            group_lane = S.vshrs(S.vci(0, T.int32), 5)
             for group_batch in T.unroll(T.ceildiv(num_groups, 64), explicit=True):
                 group_base = group_batch * 64
                 vector_base = group_batch * 32
-                inverses = S.vld(inverse_ub[group_base])
                 for vector in T.serial(T.min(32, num_vecs - vector_base)):
                     col = (vector_base + vector) * vec_size
-                    inverse_indices = S.vadds(group_lane, vector * 2)
-                    expanded_inverse = S.vselr(inverses, inverse_indices)
-                    quantized = S.vcvt(S.vmul(S.vld(values_ub[col]), expanded_inverse), T.float8_e4m3fn)
-                    S.vsts(out_ub[col], quantized, dist='PK4_B32')
+                    # Broadcast each 32-channel inverse to its 32 lanes.
+                    inverse_slots = V.vload(
+                        inverse_ub[group_base + vector * 2],
+                        size=2,
+                        group=2,
+                        stride=1,
+                    )
+                    expanded_inverse = V.vbrc(inverse_slots, size=vec_size, group=2)
+                    quantized = V.vcvt(
+                        V.vmul(V.vload(values_ub[col], size=vec_size), expanded_inverse),
+                        'float8_e4m3fn',
+                        rounding='R',
+                        saturate='NOSAT',
+                    )
+                    V.vstore(quantized, out_ub[col])
 
     @T.macro
     def load_weight(topk_weights, token_id, routed_scaling_factor):
@@ -349,8 +396,8 @@
                                     xl_ub, xr_ub, values_ub, out_ub, weight, vdiv_precision, amax_ub, use_fast_certificate, pacc_ub, count_clamp
                                 )
                                 if use_fast_certificate:
-                                    S.vsts(certificate_ub[token_offset, 0], S.vcmin(cert_min), dist='ONEPT_B32')
-                                    S.vsts(certificate_ub[token_offset, 1], S.vcmax(cert_max), dist='ONEPT_B32')
+                                    V.vstore(V.vcmin(cert_min, V.create_mask(vec_size, size=vec_size)), certificate_ub[token_offset, 0])
+                                    V.vstore(V.vcmax(cert_max, V.create_mask(vec_size, size=vec_size)), certificate_ub[token_offset, 1])
                             finish_token(
                                 values_ub, out_ub, amax_ub, inverse_ub, sf_dense_ub, out, out_sf, token_offset, token_id, acc_ub, count_clamp
                             )
@@ -386,7 +433,7 @@
             if count_clamp:
                 with T.SimdVF():
                     for i in T.unroll(3, explicit=True):
-                        S.vsts(acc_ub[i], S.vcvt(S.vcadd(S.vld(pacc_ub[i, 0])), T.int32), dist='ONEPT_B32')
+                        V.vstore(V.vcvt(V.vcadd(V.vload(pacc_ub[i, 0], size=vec_size), V.create_mask(vec_size, size=vec_size), reassoc=True), 'int32', saturate='NOSAT'), acc_ub[i])
 
                 with T.SimtVF(threads=1):
                     for i in T.serial(4):

@liuzidi

liuzidi commented Oct 26, 2026

Copy link
Copy Markdown
Author

Backward:ASC → VMI 完整源码 diff

对应 PR head 67f68202e235f03f10268872a69f4dd9b559d22a。直接比较同一提交中的 swiglu_backward_asc.py 与 swiglu_backward_vmi.py,未归一化名称、未隐藏 helper/注释/空白差异;标准 unified diff,省略的只有未变化行。ASC 文件本身没有修改。

--- a/tile_kernels/quant/swiglu_backward_asc.py
+++ b/tile_kernels/quant/swiglu_backward_vmi.py
@@ -1,13 +1,14 @@
 import tilelang
 from tilelang.ascend import language as T
 from tilelang.ascend.language import simd as S
+from tilelang.ascend.language import vmi as V
 
 from tile_kernels.quant.common import CastOutputConfig, get_packed_ue8m0_pack_factor, get_sf_shape
 from tile_kernels.utils import align
 
 
-@tilelang.jit
-def get_swiglu_backward_kernel_asc(
+@tilelang.jit(target='pto')
+def get_swiglu_backward_kernel_vmi(
     hidden: int,
     num_experts: int,
     alignment: int,
@@ -68,84 +69,28 @@
     out_sf_shape = get_sf_shape((num_expanded_tokens, hidden * 2), out_config) if with_quant else (1, 1)
     x_grad_fp8_shape = (num_expanded_tokens, hidden * 2) if with_quant else (1, 1)
 
-    @T.macro
-    def vdiv_0ulp(a, b):
-        """Correct a / b when the hardware quotient is finite; preserve signed zero."""
-        z = S.vdiv(a, b, precision='ftz_true')
-        y = S.vneg(b)
-        r = S.alloc_var(T.float32)
-        r = a
-        S.vmula(r, z, y)
-        z_pre = T.reinterpret(S.vadds(T.reinterpret(z, 'int32x64'), -1), 'float32x64')
-        z_next = T.reinterpret(S.vadds(T.reinterpret(z, 'int32x64'), 1), 'float32x64')
-        r_pre = S.alloc_var(T.float32)
-        r_pre = a
-        S.vmula(r_pre, z_pre, y)
-        r_next = S.alloc_var(T.float32)
-        r_next = a
-        S.vmula(r_next, z_next, y)
-        r = S.vabs(r)
-        r_pre = S.vabs(r_pre)
-        r_next = S.vabs(r_next)
-        take_pre = S.vcmp(r_pre, r, op='le')
-        best_r = S.vsel(r_pre, r, take_pre)
-        best_z = S.vsel(z_pre, z, take_pre)
-        take_next = S.vcmp(r_next, best_r, op='lt')
-        corrected = S.vsel(z_next, best_z, take_next)
-        is_zero = S.vcmps(z, 0.0, op='eq')
-        return S.vsel(z, corrected, is_zero)
-
-    @T.macro
-    def sigmoid_vdiv_0ulp(a, b):
-        """Correct a / b for sigmoid."""
-        z = S.vdiv(a, b, precision='ftz_true')
-        neg_b = S.vneg(b)
-
-        residual = S.alloc_var(T.float32)
-        residual = a
-        S.vmula(residual, z, neg_b)
-
-        residual_bits = T.reinterpret(residual, 'int32x64')
-        sign_mask = S.vshrs(residual_bits, 31)
-        use_bits_minus = S.vcmps(residual_bits, 0, op='lt')
-        delta = S.vor(sign_mask, S.vdup(1, T.int32))
-        adjacent = T.reinterpret(S.vadd(T.reinterpret(z, 'int32x64'), delta), 'float32x64')
-
-        adjacent_residual = S.alloc_var(T.float32)
-        adjacent_residual = a
-        S.vmula(adjacent_residual, adjacent, neg_b)
-        residual_sum = S.vadd(residual, adjacent_residual)
-        take_bits_plus = S.vcmps(residual_sum, 0.0, op='gt')
-        is_nonzero = S.vcmps(z, 0.0, op='ne')
-        take_adjacent = S.pxor(take_bits_plus, use_bits_minus, is_nonzero)
-        return S.vsel(adjacent, z, take_adjacent)
+
 
     @T.macro
     def get_scale_and_inverse(amax):
         # Compute one vector of output scales and inverse scales from per-group amax.
-        clamped_amax = S.vmaxs(amax, out_config.clamp_min_value)
+        full = V.create_mask(vec_size, size=vec_size)
+        clamped_amax = V.vmaxs(amax, T.float32(out_config.clamp_min_value), full)
         if out_config.round_sf:
-            exponent_mask = S.vdup(0x7F800000, T.uint32)
-            exponent_carry = S.vdup(0x001FFFFF, T.uint32)
-            inverse_bias = S.vdup(0x83000000, T.uint32)
-            scale_bias = S.vdup(0x04000000, T.uint32)
-            exponent = S.vand(S.vadd(T.reinterpret(clamped_amax, 'uint32x64'), exponent_carry), exponent_mask)
-            scale = T.reinterpret(S.vsub(exponent, scale_bias), 'float32x64')
-            inverse = T.reinterpret(S.vsub(inverse_bias, exponent), 'float32x64')
+            exponent_mask = V.vbrc(T.uint32(0x7F800000), size=vec_size)
+            exponent_carry = V.vbrc(T.uint32(0x001FFFFF), size=vec_size)
+            inverse_bias = V.vbrc(T.uint32(0x83000000), size=vec_size)
+            scale_bias = V.vbrc(T.uint32(0x04000000), size=vec_size)
+            exponent = V.vand(V.vadd(V.vinterpret_cast(clamped_amax, 'uint32'), exponent_carry), exponent_mask)
+            scale = V.vinterpret_cast(V.vsub(exponent, scale_bias), 'float32')
+            inverse = V.vinterpret_cast(V.vsub(inverse_bias, exponent), 'float32')
             return scale, inverse
-        quant_max = S.vdup(448.0, T.float32)
-        return S.vdiv(clamped_amax, quant_max), S.vdiv(quant_max, clamped_amax)
-
-    @T.macro
-    def store_fourfold_expanded(dst, offset, value):
-        # Repeat each compact FP32 value four times for subsequent E2B_B32 loads.
-        value_2_lo, value_2_hi = S.vintlv(value, value)
-        value_4_0, value_4_1 = S.vintlv(value_2_lo, value_2_lo)
-        value_4_2, value_4_3 = S.vintlv(value_2_hi, value_2_hi)
-        S.vsts(dst[offset], value_4_0)
-        S.vsts(dst[offset + vec_size], value_4_1)
-        S.vsts(dst[offset + vec_size * 2], value_4_2)
-        S.vsts(dst[offset + vec_size * 3], value_4_3)
+        quant_max = V.vbrc(T.float32(448.0), size=vec_size)
+        return (
+            V.vdiv(clamped_amax, quant_max, full, precision='exact'),
+            V.vdiv(quant_max, clamped_amax, full, precision='exact'),
+        )
+
 
     @T.macro
     def init_transpose_indices(indices):
@@ -153,31 +98,35 @@
         # dense token-major scales to the output layout.
         with T.SimdVF():
             if out_config.use_packed_ue8m0:
-                lane = S.vci(0, T.int16)
-                row = S.vand(T.reinterpret(lane, 'uint16x128'), S.vdup(block_m - 1, T.uint16))
-                group = T.reinterpret(S.vshrs(lane, block_m.bit_length() - 1), 'uint16x128')
-                source = S.vadd(S.vmuls(row, sf_row_stride), group)
-                S.vsts(indices[0], source, dist='NORM_B16')
+                lane = V.vci(T.int16(0), size=128)
+                row = V.vand(V.vinterpret_cast(lane, 'uint16'), V.vbrc(T.uint16(block_m - 1), size=128))
+                group = V.vinterpret_cast(V.vshr(lane, V.vbrc(T.int16(block_m.bit_length() - 1), size=128)), 'uint16')
+                source = V.vadd(V.vmul(row, V.vbrc(T.uint16(sf_row_stride), size=128)), group)
+                V.vstore(source, indices[0])
             else:
-                lane = S.vci(0, T.int32)
-                row = S.vand(T.reinterpret(lane, 'uint32x64'), S.vdup(block_m - 1, T.uint32))
-                group = T.reinterpret(S.vshrs(lane, block_m.bit_length() - 1), 'uint32x64')
-                source = S.vadd(S.vmuls(row, sf_row_stride), group)
-                S.vsts(indices[0], source, dist='NORM_B32')
+                lane = V.vci(T.int32(0), size=64)
+                row = V.vand(V.vinterpret_cast(lane, 'uint32'), V.vbrc(T.uint32(block_m - 1), size=64))
+                group = V.vinterpret_cast(V.vshr(lane, V.vbrc(T.int32(block_m.bit_length() - 1), size=64)), 'uint32')
+                source = V.vadd(V.vmul(row, V.vbrc(T.uint32(sf_row_stride), size=64)), group)
+                V.vstore(source, indices[0])
 
     @T.macro
     def transpose_sf(src, dst, indices, source_col):
         # Transpose one hidden block of dense scales into the stored layout.
         with T.SimdVF():
-            source_indices = S.vld(indices[0])
-            store_dist = 'NORM_B16' if out_config.use_packed_ue8m0 else 'NORM_B32'
+            if out_config.use_packed_ue8m0:
+                vector_lanes = 128
+            else:
+                vector_lanes = 64
+            source_indices = V.vload(indices[0], size=vector_lanes)
+            full_mask = V.create_mask(vector_lanes, size=vector_lanes)
             for output_vector in T.unroll((num_stored_block_groups + stored_groups_per_vector - 1) // stored_groups_per_vector, explicit=True):
                 group_base = output_vector * stored_groups_per_vector
-                values = S.vgather2(src[0, source_col + group_base], source_indices)
-                S.vsts(dst[group_base, 0], values, dist=store_dist)
+                values = V.vgather(src[0, source_col + group_base], source_indices, full_mask)
+                V.vstore(values, dst[group_base, 0])
 
     @T.prim_func
-    def swiglu_backward_kernel_asc(
+    def swiglu_backward_kernel_vmi(
         x: T.Tensor[(num_expanded_tokens, hidden * 2), x_dtype],
         x_sf: T.Tensor[x_sf_shape, T.float32],
         act_x_grad: T.Tensor[(num_expanded_tokens, hidden), act_x_grad_dtype],
@@ -315,147 +264,172 @@
                         T.copy(act_x_grad[token_id, col_offset : col_offset + block_k], grad_ub)
 
                         with T.SimdVF():
-                            if with_quant:
-                                for scale_vector in range(num_scale_vectors):
-                                    scale_offset = scale_vector * vec_size
-                                    expanded_offset = scale_offset * 4
-                                    x_scale_compact = S.vld(x_sf_ub[scale_offset])
-                                    y_scale_compact = S.vld(y_sf_ub[scale_offset])
-                                    store_fourfold_expanded(x_quant_scratch_ub, expanded_offset, x_scale_compact)
-                                    store_fourfold_expanded(y_quant_scratch_ub, expanded_offset, y_scale_compact)
-                                S.mem_bar('VST_VLD')
-                            ones = S.vdup(1.0, T.float32)
-                            zeros = S.vdup(0.0, T.float32)
-                            if with_quant:
-                                high_half_mask = S.vcmps(S.vci(T.int32(0), T.int32), 31, op='gt')
-                                low_half_mask = S.pset(32, 'PAT_VL32')
+                            full = V.create_mask(vec_size, size=vec_size)
+                            zeros = V.vbrc(T.float32(0.0), size=vec_size)
+                            ones = V.vbrc(T.float32(1.0), size=vec_size)
                             if with_weight:
-                                weight_acc = S.alloc_var(T.float32)
-                                weight_acc = zeros if hidden_block == 0 else S.vld(weight_acc_ub[row, 0])
-
+                                weight_acc = V.alloc_var(T.float32, size=vec_size)
+                                # hidden_block is a runtime TIR loop variable; use an
+                                # if statement so PTO does not see a vector select.
+                                if hidden_block == 0:
+                                    weight_acc = zeros
+                                else:
+                                    weight_acc = V.vload(weight_acc_ub[row, 0], size=vec_size)
+
+                            # VMI exposes separate widen/narrow conversions. PTO
+                            # codegen already supports fused T.simd dist='UNPK/PK'
+                            # forms, but #1616 forbids using ASC physical layouts
+                            # here; a VMI widen/narrow load/store is the upstream
+                            # feature request described in the PR.
                             for vector in T.serial(block_k // vec_size):
                                 col = vector * vec_size
                                 if with_quant:
-                                    x_scale = S.vld(x_quant_scratch_ub[vector * 8], dist='E2B_B32')
-                                    y_scale = S.vld(y_quant_scratch_ub[vector * 8], dist='E2B_B32')
-                                    x_raw = S.vcvt(S.vld(x_ub[col], dist='UNPK4_B8'), T.float32)
-                                    y_raw = S.vcvt(S.vld(y_ub[col], dist='UNPK4_B8'), T.float32)
-                                    x_value = S.vmul(x_raw, x_scale)
-                                    y_value = S.vmul(y_raw, y_scale)
+                                    x_scale = V.vbrc(V.vload(x_sf_ub[vector * 2], size=2, group=2, stride=1), size=vec_size, group=2)
+                                    y_scale = V.vbrc(V.vload(y_sf_ub[vector * 2], size=2, group=2, stride=1), size=vec_size, group=2)
+                                    x_raw = V.vcvt(V.vload(x_ub[col], size=vec_size), 'float32')
+                                    y_raw = V.vcvt(V.vload(y_ub[col], size=vec_size), 'float32')
+                                    x_value = V.vmul(x_raw, x_scale)
+                                    y_value = V.vmul(y_raw, y_scale)
                                 elif x_dtype == T.bfloat16:
-                                    x_value = S.vcvt(S.vld(x_ub[col], dist='UNPK_B16'), T.float32, part=0)
-                                    y_value = S.vcvt(S.vld(y_ub[col], dist='UNPK_B16'), T.float32, part=0)
-                                else:
-                                    x_value = S.vld(x_ub[col])
-                                    y_value = S.vld(y_ub[col])
+                                    x_value = V.vcvt(V.vload(x_ub[col], size=vec_size), 'float32')
+                                    y_value = V.vcvt(V.vload(y_ub[col], size=vec_size), 'float32')
+                                else:
+                                    x_value = V.vload(x_ub[col], size=vec_size)
+                                    y_value = V.vload(y_ub[col], size=vec_size)
                                 if act_x_grad_dtype == T.bfloat16:
-                                    grad = S.vcvt(S.vld(grad_ub[col], dist='UNPK_B16'), T.float32, part=0)
-                                else:
-                                    grad = S.vld(grad_ub[col])
+                                    grad = V.vcvt(V.vload(grad_ub[col], size=vec_size), 'float32')
+                                else:
+                                    grad = V.vload(grad_ub[col], size=vec_size)
 
                                 if use_clamp:
-                                    is_clamped_x = S.vcmps(x_value, clamp_value, op='gt')
-                                    is_clamped_y = S.vcmps(S.vabs(y_value), clamp_value, op='gt')
-                                    x_compute_value = S.vmins(x_value, clamp_value)
-                                    y_compute_value = S.vmins(S.vmaxs(y_value, -clamp_value), clamp_value)
+                                    is_clamped_x = V.vcmps(x_value, T.float32(clamp_value), full, 'gt')
+                                    is_clamped_y = V.vcmps(V.vabs(y_value), T.float32(clamp_value), full, 'gt')
+                                    x_compute_value = V.vmins(x_value, T.float32(clamp_value), full)
+                                    y_compute_value = V.vmins(V.vmaxs(y_value, T.float32(-clamp_value), full), T.float32(clamp_value), full)
                                 else:
                                     x_compute_value = x_value
                                     y_compute_value = y_value
 
-                                sigmoid_denominator = S.vadds(S.vexpdif(zeros, x_compute_value), 1.0)
-                                sigmoid = sigmoid_vdiv_0ulp(ones, sigmoid_denominator)
+                                sigmoid_denominator = V.vadds(V.vexpdif(zeros, x_compute_value, full), T.float32(1.0), full)
+                                sigmoid = V.vdiv(ones, sigmoid_denominator, full, precision='exact')
                                 if with_weight or do_recompute:
-                                    # Keep a separate exact division for the required FP32 rounding.
-                                    act_value = S.vmul(vdiv_0ulp(x_compute_value, sigmoid_denominator), y_compute_value)
+                                    # Keep a separate 0-ULP division for the required FP32 rounding.
+                                    act_value = V.vmul(V.vdiv(x_compute_value, sigmoid_denominator, full, precision='exact'), y_compute_value)
                                 if with_weight:
-                                    weight_contribution = S.vmul(grad, act_value)
-                                    weight_value = S.vmuls(weight_contribution, valid_token_f32) if num_experts else weight_contribution
-                                    weight_acc = S.vadd(weight_acc, weight_value)
-                                grad_ws = S.vmul(S.vmuls(grad, effective_weight), sigmoid)
-                                inner = S.vadd(ones, S.vmul(x_compute_value, S.vsub(ones, sigmoid)))
-                                x_grad_unclamped = S.vmul(S.vmul(grad_ws, y_compute_value), inner)
-                                y_grad_unclamped = S.vmul(grad_ws, x_compute_value)
-                                x_grad_value = S.vsel(zeros, x_grad_unclamped, is_clamped_x) if use_clamp else x_grad_unclamped
-                                y_grad_value = S.vsel(zeros, y_grad_unclamped, is_clamped_y) if use_clamp else y_grad_unclamped
+                                    weight_contribution = V.vmul(grad, act_value)
+                                    weight_value = V.vmuls(weight_contribution, valid_token_f32, full) if num_experts else weight_contribution
+                                    weight_acc = V.vadd(weight_acc, weight_value)
+                                grad_ws = V.vmul(V.vmuls(grad, effective_weight, full), sigmoid)
+                                inner = V.vadd(ones, V.vmul(x_compute_value, V.vsub(ones, sigmoid)))
+                                x_grad_unclamped = V.vmul(V.vmul(grad_ws, y_compute_value), inner)
+                                y_grad_unclamped = V.vmul(grad_ws, x_compute_value)
+                                x_grad_value = V.vsel(is_clamped_x, zeros, x_grad_unclamped) if use_clamp else x_grad_unclamped
+                                y_grad_value = V.vsel(is_clamped_y, zeros, y_grad_unclamped) if use_clamp else y_grad_unclamped
                                 if not with_quant:
                                     if out_dtype == T.bfloat16:
-                                        S.vsts(x_grad_ub[col], S.vcvt(x_grad_value, T.bfloat16), dist='PK_B32')
-                                        S.vsts(y_grad_ub[col], S.vcvt(y_grad_value, T.bfloat16), dist='PK_B32')
+                                        # Explicit NOSAT: same bug A/B rationale as
+                                        # the FP8 output; do not rely on PTODSL's
+                                        # implicit SAT default for narrowing.
+                                        V.vstore(V.vcvt(x_grad_value, 'bfloat16', saturate='NOSAT'), x_grad_ub[col])
+                                        V.vstore(V.vcvt(y_grad_value, 'bfloat16', saturate='NOSAT'), y_grad_ub[col])
                                     else:
-                                        S.vsts(x_grad_ub[col], x_grad_value)
-                                        S.vsts(y_grad_ub[col], y_grad_value)
-                                else:
-                                    S.vsts(x_grad_f32_ub[col], x_grad_value)
-                                    S.vsts(y_grad_f32_ub[col], y_grad_value)
-                                    x_abs = S.vabs(x_grad_value)
-                                    y_abs = S.vabs(y_grad_value)
-                                    S.vsts(x_quant_scratch_ub[vector * 2], S.vcmax(x_abs, low_half_mask), dist='ONEPT_B32')
-                                    S.vsts(x_quant_scratch_ub[vector * 2 + 1], S.vcmax(x_abs, high_half_mask), dist='ONEPT_B32')
-                                    S.vsts(y_quant_scratch_ub[vector * 2], S.vcmax(y_abs, low_half_mask), dist='ONEPT_B32')
-                                    S.vsts(y_quant_scratch_ub[vector * 2 + 1], S.vcmax(y_abs, high_half_mask), dist='ONEPT_B32')
+                                        V.vstore(x_grad_value, x_grad_ub[col])
+                                        V.vstore(y_grad_value, y_grad_ub[col])
+                                else:
+                                    V.vstore(x_grad_value, x_grad_f32_ub[col])
+                                    V.vstore(y_grad_value, y_grad_f32_ub[col])
+                                    x_abs = V.vabs(x_grad_value)
+                                    y_abs = V.vabs(y_grad_value)
+                                    V.vstore(V.vcmax(x_abs, full, group=2), x_quant_scratch_ub[vector * 2])
+                                    V.vstore(V.vcmax(y_abs, full, group=2), y_quant_scratch_ub[vector * 2])
                                 if do_recompute:
-                                    act_out_value = S.vmuls(act_value, effective_weight)
+                                    act_out_value = V.vmuls(act_value, effective_weight, full)
                                     if out_dtype == T.bfloat16:
-                                        S.vsts(act_out_ub[col], S.vcvt(act_out_value, T.bfloat16), dist='PK_B32')
+                                        # Explicit NOSAT, same bug A/B rationale.
+                                        V.vstore(V.vcvt(act_out_value, 'bfloat16', saturate='NOSAT'), act_out_ub[col])
                                     else:
-                                        S.vsts(act_out_ub[col], act_out_value)
+                                        V.vstore(act_out_value, act_out_ub[col])
 
                             if with_weight:
-                                one_mask = S.pset(32, 'PAT_VL1')
                                 if hidden_block + 1 == num_hidden_blocks:
-                                    reduced_weight_sum = S.vcadd(weight_acc)
-                                    weight_sum = S.vmuls(reduced_weight_sum, routed_scaling_factor) if with_routed_scaling else reduced_weight_sum
-                                    S.vsts(weight_out_ub[0], weight_sum, one_mask, dist='ONEPT_B32')
-                                else:
-                                    S.vsts(weight_acc_ub[row, 0], weight_acc)
+                                    # ASC S.vcadd is a pairwise reduction.
+                                    # PTODSL only accepts reassoc=True here, so
+                                    # weight_grad can differ in the last mantissa
+                                    # bits; it is validated at the repo's 1e-4.
+                                    one_mask = V.create_mask(1, size=1)
+                                    reduced_weight_sum = V.vcadd(weight_acc, full, reassoc=True)
+                                    weight_sum = (
+                                        V.vmuls(reduced_weight_sum, T.float32(routed_scaling_factor), one_mask)
+                                        if with_routed_scaling
+                                        else reduced_weight_sum
+                                    )
+                                    V.vstore(weight_sum, weight_out_ub[0], group=1, stride=1)
+                                else:
+                                    V.vstore(weight_acc, weight_acc_ub[row, 0])
 
                             if with_quant:
                                 S.mem_bar('VST_VLD')
                                 if num_experts:
-                                    valid_token_vec = S.vdup(valid_token_f32, T.float32)
-                                # Preserve unread amax while overwriting the scratch with inverses.
-                                for scale_vector_index in range(num_scale_vectors):
-                                    scale_vector = num_scale_vectors - 1 - scale_vector_index
+                                    valid_token_vec = V.vbrc(valid_token_f32, size=vec_size)
+                                # Each iteration reads its amax before overwriting
+                                # the same slot with the inverse, so order is free.
+                                for scale_vector in range(num_scale_vectors):
                                     scale_offset = scale_vector * vec_size
-                                    inverse_offset = scale_offset * 4
-                                    x_scale_value, x_inv = get_scale_and_inverse(S.vld(x_quant_scratch_ub[scale_offset]))
-                                    y_scale_value, y_inv = get_scale_and_inverse(S.vld(y_quant_scratch_ub[scale_offset]))
-                                    store_fourfold_expanded(x_quant_scratch_ub, inverse_offset, x_inv)
-                                    store_fourfold_expanded(y_quant_scratch_ub, inverse_offset, y_inv)
-                                    x_scale_out = S.vmul(x_scale_value, valid_token_vec) if num_experts else x_scale_value
-                                    y_scale_out = S.vmul(y_scale_value, valid_token_vec) if num_experts else y_scale_value
+                                    x_scale_value, x_inv = get_scale_and_inverse(V.vload(x_quant_scratch_ub[scale_offset], size=vec_size))
+                                    y_scale_value, y_inv = get_scale_and_inverse(V.vload(y_quant_scratch_ub[scale_offset], size=vec_size))
+                                    V.vstore(x_inv, x_quant_scratch_ub[scale_offset])
+                                    V.vstore(y_inv, y_quant_scratch_ub[scale_offset])
+                                    x_scale_out = V.vmul(x_scale_value, valid_token_vec) if num_experts else x_scale_value
+                                    y_scale_out = V.vmul(y_scale_value, valid_token_vec) if num_experts else y_scale_value
                                     if out_config.use_packed_ue8m0:
-                                        S.vsts(
+                                        x_exp = V.vshrs(V.vinterpret_cast(x_scale_out, 'uint32'), T.uint32(23), full)
+                                        y_exp = V.vshrs(V.vinterpret_cast(y_scale_out, 'uint32'), T.uint32(23), full)
+                                        # int-narrow (uint32 -> uint8): NOSAT is
+                                        # exactly ASC's reinterpret-truncation;
+                                        # unrelated to the FP8 output saturate
+                                        # decision below. Spell it out because PTODSL
+                                        # otherwise defaults to SAT for narrowing,
+                                        # which differs for exponents >= 256
+                                        # (amax = inf).
+                                        V.vstore(
+                                            V.vcvt(x_exp, 'uint8', saturate='NOSAT'),
                                             sf_dense_u8[row, sf_block_offset * 2 + scale_offset],
-                                            T.reinterpret(S.vshrs(T.reinterpret(x_scale_out, 'uint32x64'), 23), 'uint8x256'),
-                                            dist='PK4_B32',
                                         )
-                                        S.vsts(
+                                        V.vstore(
+                                            V.vcvt(y_exp, 'uint8', saturate='NOSAT'),
                                             sf_dense_u8[row, sf_half_stride * 2 + sf_block_offset * 2 + scale_offset],
-                                            T.reinterpret(S.vshrs(T.reinterpret(y_scale_out, 'uint32x64'), 23), 'uint8x256'),
-                                            dist='PK4_B32',
                                         )
                                     else:
-                                        S.vsts(sf_dense_ub[row, sf_block_offset + scale_offset], x_scale_out)
-                                        S.vsts(sf_dense_ub[row, sf_half_stride + sf_block_offset + scale_offset], y_scale_out)
+                                        V.vstore(x_scale_out, sf_dense_ub[row, sf_block_offset + scale_offset])
+                                        V.vstore(y_scale_out, sf_dense_ub[row, sf_half_stride + sf_block_offset + scale_offset])
 
                                 S.mem_bar('VST_VLD')
                                 if num_experts:
-                                    valid_token_mask = S.vcmps(valid_token_vec, 0.0, op='gt')
+                                    valid_token_mask = V.vcmps(valid_token_vec, T.float32(0.0), full, 'gt')
                                 for vector in T.serial(block_k // vec_size):
                                     col = vector * vec_size
-                                    x_inv_vec = S.vld(x_quant_scratch_ub[vector * 8], dist='E2B_B32')
-                                    y_inv_vec = S.vld(y_quant_scratch_ub[vector * 8], dist='E2B_B32')
-                                    x_grad_quant_value = S.vld(x_grad_f32_ub[col])
-                                    y_grad_quant_value = S.vld(y_grad_f32_ub[col])
-                                    x_scaled_value = S.vmul(x_grad_quant_value, x_inv_vec)
-                                    y_scaled_value = S.vmul(y_grad_quant_value, y_inv_vec)
-                                    S.vsts(x_grad_ub[col], S.vcvt(x_grad_quant_value, T.bfloat16), dist='PK_B32')
-                                    S.vsts(y_grad_ub[col], S.vcvt(y_grad_quant_value, T.bfloat16), dist='PK_B32')
-                                    x_scaled = S.vsel(x_scaled_value, zeros, valid_token_mask) if num_experts else x_scaled_value
-                                    y_scaled = S.vsel(y_scaled_value, zeros, valid_token_mask) if num_experts else y_scaled_value
-                                    S.vsts(x_grad_fp8_ub[col], S.vcvt(x_scaled, T.float8_e4m3fn), dist='PK4_B32')
-                                    S.vsts(y_grad_fp8_ub[col], S.vcvt(y_scaled, T.float8_e4m3fn), dist='PK4_B32')
+                                    x_inv_vec = V.vbrc(V.vload(x_quant_scratch_ub[vector * 2], size=2, group=2, stride=1), size=vec_size, group=2)
+                                    y_inv_vec = V.vbrc(V.vload(y_quant_scratch_ub[vector * 2], size=2, group=2, stride=1), size=vec_size, group=2)
+                                    x_grad_quant_value = V.vload(x_grad_f32_ub[col], size=vec_size)
+                                    y_grad_quant_value = V.vload(y_grad_f32_ub[col], size=vec_size)
+                                    x_scaled_value = V.vmul(x_grad_quant_value, x_inv_vec)
+                                    y_scaled_value = V.vmul(y_grad_quant_value, y_inv_vec)
+                                    # Explicit NOSAT, same bug A/B rationale.
+                                    V.vstore(V.vcvt(x_grad_quant_value, 'bfloat16', saturate='NOSAT'), x_grad_ub[col])
+                                    V.vstore(V.vcvt(y_grad_quant_value, 'bfloat16', saturate='NOSAT'), y_grad_ub[col])
+                                    x_scaled = V.vsel(valid_token_mask, x_scaled_value, zeros) if num_experts else x_scaled_value
+                                    y_scaled = V.vsel(valid_token_mask, y_scaled_value, zeros) if num_experts else y_scaled_value
+                                    # FP8 output saturate: explicit NOSAT. ASC's
+                                    # RS_ENABLE is a no-op for f32->e4m3 here (bug A),
+                                    # while VMI SAT maps NaN to 0x00 (bug B). NOSAT is
+                                    # the only mode that both reproduces ASC today and
+                                    # preserves NaN; switch back to SAT after bug A is
+                                    # fixed upstream. Both bugs: upstream
+                                    # (draft ready; to be filed), repro_fp8_saturate.py.
+                                    # Finite values are
+                                    # unaffected: value <= 448*(1+2^-23) << 464.
+                                    V.vstore(V.vcvt(x_scaled, 'float8_e4m3fn', rounding='R', saturate='NOSAT'), x_grad_fp8_ub[col])
+                                    V.vstore(V.vcvt(y_scaled, 'float8_e4m3fn', rounding='R', saturate='NOSAT'), y_grad_fp8_ub[col])
 
                         T.copy(x_grad_ub, x_grad[token_id, col_offset : col_offset + block_k])
                         T.copy(y_grad_ub, x_grad[token_id, hidden + col_offset : hidden + col_offset + block_k])
@@ -497,4 +471,4 @@
                                 output_sf[token_base, num_stored_groups + sf_output_offset],
                             )
 
-    return swiglu_backward_kernel_asc
+    return swiglu_backward_kernel_vmi

@liuzidi

liuzidi commented Oct 26, 2026

Copy link
Copy Markdown
Author

ASC → VMI:两个公开入口的完整 diff

对应 PR head 67f68202e235f03f10268872a69f4dd9b559d22a。与前两条 forward/backward 完整对照合起来,覆盖本 PR 的全部四个文件;入口只切换 Ascend 工厂的 import 和调用。

diff --git a/tile_kernels/quant/swiglu_backward_kernel.py b/tile_kernels/quant/swiglu_backward_kernel.py
index cc65f9e..f2edc00 100644
--- a/tile_kernels/quant/swiglu_backward_kernel.py
+++ b/tile_kernels/quant/swiglu_backward_kernel.py
@@ -107,9 +107,9 @@ def swiglu_backward_and_per_token_cast_impl(
 
     # Get kernel implement
     if is_ascend():
-        from tile_kernels.quant.swiglu_backward_asc import get_swiglu_backward_kernel_asc
+        from tile_kernels.quant.swiglu_backward_vmi import get_swiglu_backward_kernel_vmi
 
-        kernel = get_swiglu_backward_kernel_asc(
+        kernel = get_swiglu_backward_kernel_vmi(
             hidden,
             num_experts,
             get_token_alignment(),
diff --git a/tile_kernels/quant/swiglu_forward_kernel.py b/tile_kernels/quant/swiglu_forward_kernel.py
index a602be8..9285d17 100644
--- a/tile_kernels/quant/swiglu_forward_kernel.py
+++ b/tile_kernels/quant/swiglu_forward_kernel.py
@@ -46,9 +46,9 @@ def swiglu_forward_and_per_token_cast_impl(
 
     # Get kernel implement
     if is_ascend():
-        from tile_kernels.quant.swiglu_forward_asc import get_swiglu_forward_kernel_asc
+        from tile_kernels.quant.swiglu_forward_vmi import get_swiglu_forward_kernel_vmi
 
-        kernel = get_swiglu_forward_kernel_asc(
+        kernel = get_swiglu_forward_kernel_vmi(
             hidden,
             num_experts,
             get_token_alignment(),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant