#!/usr/bin/env python # coding=utf-8 """ Unit test for SkipLoRA feature in AMX_SFT_MOE_TP. This test verifies that when SkipLoRA=true (method="AMXBF16_SFT_SkipLoRA"): 1. Forward pass works identically (LoRA is still used in forward) 2. Backward pass only computes base weight contribution to grad_input 3. LoRA weight gradients are NOT computed (should remain zero) Usage: python test_skip_lora.py [--tp-count 1] [--threshold 0.05] """ import os import sys import argparse import numpy as np sys.path.insert(0, os.path.dirname(__file__) + "/../build") import torch # Try to import kt_kernel try: from kt_kernel.experts import KTMoEWrapper HAS_KT_KERNEL = True except ImportError as e: HAS_KT_KERNEL = False print(f"WARNING: kt_kernel not available: {e}") # ============================================================================ # Configuration # ============================================================================ DEFAULT_TP_COUNT = 1 DEFAULT_THRESHOLD = 0.05 # Test dimensions (smaller for faster testing) TEST_CONFIG = { "expert_num": 8, "hidden_size": 256, # Smaller for faster testing "intermediate_size": 512, "qlen": 32, "k": 2, "num_threads": 8, "max_len": 256, } # LoRA configuration LORA_RANK = 8 LORA_ALPHA = 16 LORA_SCALING = LORA_ALPHA / LORA_RANK # Weight scaling for numerical stability WEIGHT_SCALE = 0.01 INPUT_SCALE = 0.1 # ============================================================================ # Python Reference Implementation # ============================================================================ def silu(x): """SiLU activation function""" return x * torch.sigmoid(x) def silu_backward(gate_out, up_out, grad_intermediate): """Backward pass for SiLU activation""" sigmoid_gate = torch.sigmoid(gate_out) silu_gate = gate_out * sigmoid_gate grad_up_out = grad_intermediate * silu_gate silu_grad = sigmoid_gate * (1 + gate_out - gate_out * sigmoid_gate) grad_gate_out = grad_intermediate * up_out * silu_grad return grad_gate_out, grad_up_out class PythonMoEReference: """Python reference implementation for MoE with LoRA""" def __init__( self, gate_proj, up_proj, down_proj, gate_lora_a, gate_lora_b, up_lora_a, up_lora_b, down_lora_a, down_lora_b, lora_scaling, ): self.gate_proj = gate_proj.float() self.up_proj = up_proj.float() self.down_proj = down_proj.float() self.gate_lora_a = gate_lora_a.float() self.gate_lora_b = gate_lora_b.float() self.up_lora_a = up_lora_a.float() self.up_lora_b = up_lora_b.float() self.down_lora_a = down_lora_a.float() self.down_lora_b = down_lora_b.float() self.lora_scaling = lora_scaling self.expert_num = gate_proj.shape[0] self.intermediate_size = gate_proj.shape[1] self.hidden_size = gate_proj.shape[2] self.lora_rank = gate_lora_a.shape[1] def forward_with_cache(self, input_tensor, expert_ids, routing_weights): """Forward pass returning cache for backward""" qlen = input_tensor.shape[0] k = expert_ids.shape[1] input_f = input_tensor.float() output = torch.zeros(qlen, self.hidden_size, dtype=torch.float32) forward_cache = {} for i in range(qlen): for j in range(k): eid = expert_ids[i, j].item() weight = routing_weights[i, j].item() x = input_f[i : i + 1] # Gate gate_base = torch.mm(x, self.gate_proj[eid].t()) gate_lora = torch.mm(torch.mm(x, self.gate_lora_a[eid].t()), self.gate_lora_b[eid].t()) gate_out = gate_base + gate_lora * self.lora_scaling # Up up_base = torch.mm(x, self.up_proj[eid].t()) up_lora = torch.mm(torch.mm(x, self.up_lora_a[eid].t()), self.up_lora_b[eid].t()) up_out = up_base + up_lora * self.lora_scaling # Activation act_out = silu(gate_out) * up_out # Down down_base = torch.mm(act_out, self.down_proj[eid].t()) down_lora = torch.mm(torch.mm(act_out, self.down_lora_a[eid].t()), self.down_lora_b[eid].t()) down_out = down_base + down_lora * self.lora_scaling output[i] += down_out.squeeze() * weight # Store cache cache_key = f"e{eid}_i{i}_j{j}" forward_cache[cache_key] = {"gate_out": gate_out, "up_out": up_out, "act_out": act_out} return output, forward_cache def backward_base_only(self, grad_output, input_tensor, expert_ids, routing_weights, forward_cache): """ Backward pass computing ONLY base weight contribution to grad_input. This is what SkipLoRA should produce. """ qlen = input_tensor.shape[0] k = expert_ids.shape[1] grad_output_f = grad_output.float() grad_input = torch.zeros(qlen, self.hidden_size, dtype=torch.float32) for i in range(qlen): for j in range(k): eid = expert_ids[i, j].item() weight = routing_weights[i, j].item() grad_out = grad_output_f[i : i + 1] * weight # Get forward cache cache_key = f"e{eid}_i{i}_j{j}" gate_out = forward_cache[cache_key]["gate_out"] up_out = forward_cache[cache_key]["up_out"] # backward_down: grad_intermediate = grad_out @ down_proj (base only) grad_intermediate = torch.mm(grad_out, self.down_proj[eid]) # backward_activation grad_gate_out, grad_up_out = silu_backward(gate_out, up_out, grad_intermediate) # backward_gate_up: grad_input = grad_gate_out @ gate_proj + grad_up_out @ up_proj (base only) grad_input_gate = torch.mm(grad_gate_out, self.gate_proj[eid]) grad_input_up = torch.mm(grad_up_out, self.up_proj[eid]) grad_input[i] += (grad_input_gate + grad_input_up).squeeze() return grad_input def backward_full(self, grad_output, input_tensor, expert_ids, routing_weights, forward_cache): """ Full backward pass including LoRA contribution to grad_input. """ qlen = input_tensor.shape[0] k = expert_ids.shape[1] grad_output_f = grad_output.float() grad_input = torch.zeros(qlen, self.hidden_size, dtype=torch.float32) for i in range(qlen): for j in range(k): eid = expert_ids[i, j].item() weight = routing_weights[i, j].item() grad_out = grad_output_f[i : i + 1] * weight # Get forward cache cache_key = f"e{eid}_i{i}_j{j}" gate_out = forward_cache[cache_key]["gate_out"] up_out = forward_cache[cache_key]["up_out"] # backward_down: grad_intermediate = grad_out @ down_proj (base only, LoRA doesn't contribute) grad_intermediate = torch.mm(grad_out, self.down_proj[eid]) # backward_activation grad_gate_out, grad_up_out = silu_backward(gate_out, up_out, grad_intermediate) # backward_gate_up: include LoRA contribution # Base grad_input_gate_base = torch.mm(grad_gate_out, self.gate_proj[eid]) grad_input_up_base = torch.mm(grad_up_out, self.up_proj[eid]) # LoRA grad_input_gate_lora = ( torch.mm(torch.mm(grad_gate_out, self.gate_lora_b[eid]), self.gate_lora_a[eid]) * self.lora_scaling ) grad_input_up_lora = ( torch.mm(torch.mm(grad_up_out, self.up_lora_b[eid]), self.up_lora_a[eid]) * self.lora_scaling ) grad_input[i] += ( grad_input_gate_base + grad_input_up_base + grad_input_gate_lora + grad_input_up_lora ).squeeze() return grad_input # ============================================================================ # Test Functions # ============================================================================ def compare_tensors(name, tensor1, tensor2, threshold): """Compare two tensors and print results""" t1 = tensor1.float().numpy() if isinstance(tensor1, torch.Tensor) else tensor1 t2 = tensor2.float().numpy() if isinstance(tensor2, torch.Tensor) else tensor2 if t1.shape != t2.shape: print(f"\033[91m[FAIL]\033[0m {name} - Shape mismatch: {t1.shape} vs {t2.shape}") return False abs_diff = np.abs(t1 - t2) max_abs_diff = np.max(abs_diff) mean_abs_diff = np.mean(abs_diff) rel_error = mean_abs_diff / (np.mean(np.abs(t2)) + 1e-12) t1_nan = np.sum(np.isnan(t1)) t1_inf = np.sum(np.isinf(t1)) passed = rel_error < threshold and t1_nan == 0 and t1_inf == 0 if passed: print(f"\033[92m[PASS]\033[0m {name} - rel_error: {rel_error:.2e}, max_abs_diff: {max_abs_diff:.2e}") else: print(f"\033[91m[FAIL]\033[0m {name} - rel_error: {rel_error:.2e}, max_abs_diff: {max_abs_diff:.2e}") print(f" t1 mean: {np.mean(t1):.6e}, t2 mean: {np.mean(t2):.6e}") print(f" t1 NaN: {t1_nan}, Inf: {t1_inf}") return passed def check_zeros(name, tensor, threshold=1e-10): """Check if tensor is all zeros""" t = tensor.float().numpy() if isinstance(tensor, torch.Tensor) else tensor max_val = np.max(np.abs(t)) if max_val < threshold: print(f"\033[92m[PASS]\033[0m {name} - All zeros (max: {max_val:.2e})") return True else: print(f"\033[91m[FAIL]\033[0m {name} - NOT all zeros (max: {max_val:.2e}, mean: {np.mean(np.abs(t)):.2e})") return False def test_skip_lora(tp_count, threshold): """Main test function for SkipLoRA""" print("=" * 80) print("Testing SkipLoRA Feature") print("=" * 80) if not HAS_KT_KERNEL: print("\033[91mERROR: kt_kernel not available, cannot run test\033[0m") return False config = TEST_CONFIG torch.manual_seed(42) # Initialize weights print("\n[1] Initializing weights...") gate_proj = ( torch.rand(config["expert_num"], config["intermediate_size"], config["hidden_size"], dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() up_proj = ( torch.rand(config["expert_num"], config["intermediate_size"], config["hidden_size"], dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() down_proj = ( torch.rand(config["expert_num"], config["hidden_size"], config["intermediate_size"], dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() gate_lora_a = ( torch.rand(config["expert_num"], LORA_RANK, config["hidden_size"], dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() gate_lora_b = ( torch.rand(config["expert_num"], config["intermediate_size"], LORA_RANK, dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() up_lora_a = ( torch.rand(config["expert_num"], LORA_RANK, config["hidden_size"], dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() up_lora_b = ( torch.rand(config["expert_num"], config["intermediate_size"], LORA_RANK, dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() down_lora_a = ( torch.rand(config["expert_num"], LORA_RANK, config["intermediate_size"], dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() down_lora_b = ( torch.rand(config["expert_num"], config["hidden_size"], LORA_RANK, dtype=torch.bfloat16) * WEIGHT_SCALE ).contiguous() # Generate test data print("\n[2] Generating test data...") input_tensor = ( torch.rand((config["qlen"], config["hidden_size"]), dtype=torch.bfloat16) * INPUT_SCALE ).contiguous() expert_ids = torch.stack( [torch.randperm(config["expert_num"])[: config["k"]] for _ in range(config["qlen"])] ).contiguous() routing_weights = torch.rand(config["qlen"], config["k"], dtype=torch.float).contiguous() routing_weights = routing_weights / routing_weights.sum(dim=-1, keepdim=True) grad_output = (torch.rand((config["qlen"], config["hidden_size"]), dtype=torch.bfloat16) * INPUT_SCALE).contiguous() print(f" Input shape: {input_tensor.shape}") print(f" Expert IDs shape: {expert_ids.shape}") print(f" Grad output shape: {grad_output.shape}") # Create Python reference print("\n[3] Creating Python reference...") py_ref = PythonMoEReference( gate_proj, up_proj, down_proj, gate_lora_a, gate_lora_b, up_lora_a, up_lora_b, down_lora_a, down_lora_b, LORA_SCALING, ) # Run Python reference forward and backward print("\n[4] Running Python reference...") py_output, py_cache = py_ref.forward_with_cache(input_tensor, expert_ids, routing_weights) py_grad_input_base_only = py_ref.backward_base_only( grad_output, input_tensor, expert_ids, routing_weights, py_cache ) py_grad_input_full = py_ref.backward_full(grad_output, input_tensor, expert_ids, routing_weights, py_cache) print(f" Python forward output mean: {py_output.mean():.6e}") print(f" Python grad_input (base only) mean: {py_grad_input_base_only.mean():.6e}") print(f" Python grad_input (full) mean: {py_grad_input_full.mean():.6e}") # Create KTMoEWrapper instances print("\n[5] Creating C++ MoE instances via KTMoEWrapper...") # Create normal MoE (AMXBF16_SFT) wrapper_normal = KTMoEWrapper( layer_idx=0, num_experts=config["expert_num"], num_experts_per_tok=config["k"], hidden_size=config["hidden_size"], moe_intermediate_size=config["intermediate_size"], num_gpu_experts=0, cpuinfer_threads=config["num_threads"], threadpool_count=tp_count, weight_path="", chunked_prefill_size=config["max_len"], method="AMXBF16_SFT", mode="sft", lora_rank=LORA_RANK, lora_alpha=LORA_ALPHA, max_cache_depth=2, ) # Create SkipLoRA MoE (AMXBF16_SFT_SkipLoRA) wrapper_skip = KTMoEWrapper( layer_idx=0, num_experts=config["expert_num"], num_experts_per_tok=config["k"], hidden_size=config["hidden_size"], moe_intermediate_size=config["intermediate_size"], num_gpu_experts=0, cpuinfer_threads=config["num_threads"], threadpool_count=tp_count, weight_path="", chunked_prefill_size=config["max_len"], method="AMXBF16_SFT_SkipLoRA", mode="sft", lora_rank=LORA_RANK, lora_alpha=LORA_ALPHA, max_cache_depth=2, ) # Load weights print("\n[6] Loading weights...") physical_to_logical_map = torch.arange(config["expert_num"], dtype=torch.int64) wrapper_normal.gate_proj = gate_proj wrapper_normal.up_proj = up_proj wrapper_normal.down_proj = down_proj wrapper_normal.load_weights(physical_to_logical_map) wrapper_normal.init_lora_weights(gate_lora_a, gate_lora_b, up_lora_a, up_lora_b, down_lora_a, down_lora_b) wrapper_skip.gate_proj = gate_proj wrapper_skip.up_proj = up_proj wrapper_skip.down_proj = down_proj wrapper_skip.load_weights(physical_to_logical_map) wrapper_skip.init_lora_weights(gate_lora_a, gate_lora_b, up_lora_a, up_lora_b, down_lora_a, down_lora_b) # Run forward on both print("\n[7] Running C++ forward...") output_normal = wrapper_normal.forward(input_tensor, expert_ids, routing_weights, save_for_backward=True) output_skip = wrapper_skip.forward(input_tensor, expert_ids, routing_weights, save_for_backward=True) print(f" Normal forward output mean: {output_normal.float().mean():.6e}") print(f" SkipLoRA forward output mean: {output_skip.float().mean():.6e}") # Run backward on both print("\n[8] Running C++ backward...") grad_input_normal, grad_loras_normal = wrapper_normal.backward(grad_output) grad_input_skip, grad_loras_skip = wrapper_skip.backward(grad_output) print(f" Normal grad_input mean: {grad_input_normal.float().mean():.6e}") print(f" SkipLoRA grad_input mean: {grad_input_skip.float().mean():.6e}") # ============================================================================ # Comparisons # ============================================================================ print("\n" + "=" * 80) print("Comparison Results") print("=" * 80) all_passed = True # Test 1: Forward outputs should be identical print("\n[Test 1] Forward output comparison (normal vs SkipLoRA)") passed = compare_tensors("Forward output (normal vs skip)", output_normal, output_skip, threshold) all_passed = all_passed and passed # Test 2: Forward output vs Python reference print("\n[Test 2] Forward output vs Python reference") passed = compare_tensors("Forward output (C++ normal vs Python)", output_normal, py_output, threshold) all_passed = all_passed and passed # Test 3: Normal backward grad_input should match Python full backward print("\n[Test 3] Normal backward grad_input vs Python full backward") passed = compare_tensors("grad_input (C++ normal vs Python full)", grad_input_normal, py_grad_input_full, threshold) all_passed = all_passed and passed # Test 4: SkipLoRA backward grad_input should match Python base-only backward print("\n[Test 4] SkipLoRA backward grad_input vs Python base-only backward") passed = compare_tensors( "grad_input (C++ skip vs Python base-only)", grad_input_skip, py_grad_input_base_only, threshold ) all_passed = all_passed and passed # Test 5: SkipLoRA should have zero LoRA gradients print("\n[Test 5] SkipLoRA LoRA gradients should be zero") passed = check_zeros("grad_gate_lora_a (skip)", grad_loras_skip["grad_gate_lora_a"]) all_passed = all_passed and passed passed = check_zeros("grad_gate_lora_b (skip)", grad_loras_skip["grad_gate_lora_b"]) all_passed = all_passed and passed passed = check_zeros("grad_up_lora_a (skip)", grad_loras_skip["grad_up_lora_a"]) all_passed = all_passed and passed passed = check_zeros("grad_up_lora_b (skip)", grad_loras_skip["grad_up_lora_b"]) all_passed = all_passed and passed passed = check_zeros("grad_down_lora_a (skip)", grad_loras_skip["grad_down_lora_a"]) all_passed = all_passed and passed passed = check_zeros("grad_down_lora_b (skip)", grad_loras_skip["grad_down_lora_b"]) all_passed = all_passed and passed # Test 6: Normal should have non-zero LoRA gradients print("\n[Test 6] Normal LoRA gradients should be non-zero") normal_lora_grad_sum = ( grad_loras_normal["grad_gate_lora_a"].abs().sum() + grad_loras_normal["grad_gate_lora_b"].abs().sum() + grad_loras_normal["grad_up_lora_a"].abs().sum() + grad_loras_normal["grad_up_lora_b"].abs().sum() + grad_loras_normal["grad_down_lora_a"].abs().sum() + grad_loras_normal["grad_down_lora_b"].abs().sum() ) if normal_lora_grad_sum > 1e-6: print(f"\033[92m[PASS]\033[0m Normal LoRA gradients are non-zero (sum: {normal_lora_grad_sum:.6e})") else: print(f"\033[91m[FAIL]\033[0m Normal LoRA gradients are unexpectedly zero (sum: {normal_lora_grad_sum:.6e})") all_passed = False # Summary print("\n" + "=" * 80) if all_passed: print("\033[92mALL TESTS PASSED\033[0m") else: print("\033[91mSOME TESTS FAILED\033[0m") print("=" * 80) return all_passed def main(): parser = argparse.ArgumentParser(description="Test SkipLoRA feature") parser.add_argument("--tp-count", type=int, default=DEFAULT_TP_COUNT, help="TP partition count") parser.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD, help="Relative error threshold") args = parser.parse_args() success = test_skip_lora(args.tp_count, args.threshold) sys.exit(0 if success else 1) if __name__ == "__main__": main()