chore: import upstream snapshot with attribution
Book-CI / test (macos-latest) (push) Has been cancelled
Book-CI / test (ubuntu-latest) (push) Has been cancelled
Book-CI / test (windows-latest) (push) Has been cancelled
Release Fake Tag / publish (push) Has been cancelled
Deploy / deploy (macos-latest) (push) Has been cancelled
Deploy / deploy (ubuntu-latest) (push) Has been cancelled
Deploy / deploy (windows-latest) (push) Has been cancelled
Release to PyPI / Build & publish sglang-kt (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.11) (push) Has been cancelled
Release to PyPI / Build kt-kernel (Python 3.12) (push) Has been cancelled
Release to PyPI / Publish kt-kernel to PyPI (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:30:03 +08:00
commit ec436095dd
1232 changed files with 404407 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
"""Per-commit tests for KT-Kernel.
Tests in this directory are run on every commit in CI.
"""
@@ -0,0 +1,36 @@
"""AMD/ROCm backend tests for KT-Kernel (Placeholder).
This file is a placeholder for future AMD/ROCm backend tests.
Currently, KT-Kernel focuses on CPU optimizations (Intel AMX/AVX512).
To implement AMD tests:
1. Add actual test functions with @pytest.mark.amd
2. Update the estimated time in register_amd_ci()
3. Implement AMD/ROCm-specific initialization and validation tests
"""
import os
import sys
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_amd_ci
# Register this test for AMD CI (estimated time: 10 seconds, placeholder)
# Update suite name when implementing: currently using "stage-a-test-1"
register_amd_ci(est_time=10, suite="stage-a-test-1")
def test_amd_placeholder():
"""Placeholder test for AMD/ROCm backend.
TODO: Implement actual AMD/ROCm tests when AMD support is added to kt-kernel.
"""
# Currently a no-op placeholder
pass
if __name__ == "__main__":
# Allow running standalone (required by test runner)
print("⚠ AMD/ROCm tests are not yet implemented (placeholder)")
print("✓ Placeholder test passed")
@@ -0,0 +1,82 @@
"""Basic CPU backend tests for KT-Kernel.
These tests verify basic functionality without requiring model files.
"""
import os
import sys
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 30 seconds
register_cpu_ci(est_time=30, suite="default")
# Check if kt_kernel_ext is available
try:
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
HAS_KT_KERNEL = True
except ImportError:
HAS_KT_KERNEL = False
kt_kernel_ext = None
@pytest.mark.cpu
def test_kt_kernel_import():
"""Test that kt_kernel_ext can be imported."""
if not HAS_KT_KERNEL:
pytest.skip("kt_kernel_ext not built or available")
assert kt_kernel_ext is not None, "kt_kernel_ext module should be importable"
@pytest.mark.cpu
def test_cpu_infer_initialization():
"""Test that CPUInfer can be initialized."""
if not HAS_KT_KERNEL:
pytest.skip("kt_kernel_ext not built or available")
# Initialize CPUInfer with 4 threads
cpuinfer = kt_kernel_ext.CPUInfer(4)
assert cpuinfer is not None, "CPUInfer should be initialized successfully"
@pytest.mark.cpu
def test_basic_module_attributes():
"""Test that kt_kernel_ext has expected attributes."""
if not HAS_KT_KERNEL:
pytest.skip("kt_kernel_ext not built or available")
# Check for key attributes/functions
assert hasattr(kt_kernel_ext, "CPUInfer"), "kt_kernel_ext should have CPUInfer class"
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_KT_KERNEL:
print("⚠ kt_kernel_ext not available, skipping tests")
return
try:
test_kt_kernel_import()
print("✓ test_kt_kernel_import passed")
test_cpu_infer_initialization()
print("✓ test_cpu_infer_initialization passed")
test_basic_module_attributes()
print("✓ test_basic_module_attributes passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
sys.exit(1)
if __name__ == "__main__":
# Allow running standalone (required by test runner)
run_all_tests()
@@ -0,0 +1,373 @@
import importlib.util
import os
import sys
from pathlib import Path
import pytest
import torch
from safetensors.torch import load_file, save_file
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="default")
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "scripts"
/ "convert_kt_to_sglang_adapter.py"
)
SPEC = importlib.util.spec_from_file_location("convert_kt_to_sglang_adapter", SCRIPT_PATH)
converter = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(converter)
def _write_full_fused_checkpoint(path: Path, *, rank: int = 3) -> dict[str, torch.Tensor]:
e, h, i = 2, 5, 7
tensors = {
"layers.2.experts.gate_lora_a": torch.arange(e * rank * h, dtype=torch.float32).reshape(e, rank, h),
"layers.2.experts.gate_lora_b": torch.arange(e * i * rank, dtype=torch.float32).reshape(e, i, rank),
"layers.2.experts.up_lora_a": torch.arange(e * rank * h, dtype=torch.float32).reshape(e, rank, h) + 100,
"layers.2.experts.up_lora_b": torch.arange(e * i * rank, dtype=torch.float32).reshape(e, i, rank) + 200,
"layers.2.experts.down_lora_a": torch.arange(e * rank * i, dtype=torch.float32).reshape(e, rank, i) + 300,
"layers.2.experts.down_lora_b": torch.arange(e * h * rank, dtype=torch.float32).reshape(e, h, rank) + 400,
}
save_file(tensors, str(path / converter.FUSED_EXPERT_LORA_FILE))
return tensors
def test_convert_fused_expert_lora_shapes_keys_and_config(tmp_path):
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
input_dir.mkdir()
fused = _write_full_fused_checkpoint(input_dir)
summary = converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
lora_alpha=16,
)
out = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
assert summary["tensor_count"] == 12
assert summary["rank"] == 3
assert summary["target_modules"] == ["gate_proj", "up_proj", "down_proj"]
key = "model.layers.2.mlp.experts.1.gate_proj.lora_A.weight"
assert out[key].shape == (3, 5)
torch.testing.assert_close(out[key], fused["layers.2.experts.gate_lora_a"][1])
key = "model.layers.2.mlp.experts.0.down_proj.lora_B.weight"
assert out[key].shape == (5, 3)
torch.testing.assert_close(out[key], fused["layers.2.experts.down_lora_b"][0])
config = converter._load_json(output_dir / converter.ADAPTER_CONFIG_FILE)
assert config["peft_type"] == "LORA"
assert config["r"] == 3
assert config["lora_alpha"] == 16
assert config["base_model_name_or_path"] == "/models/base"
assert config["target_modules"] == ["gate_proj", "up_proj", "down_proj"]
def test_merges_existing_adapter_and_prefers_existing_lora_alpha(tmp_path):
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir)
existing_tensor = torch.ones(3, 5)
save_file(
{
"base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": existing_tensor,
},
str(input_dir / converter.ADAPTER_MODEL_FILE),
)
converter._write_json(
input_dir / converter.ADAPTER_CONFIG_FILE,
{
"peft_type": "LORA",
"r": 3,
"lora_alpha": 9,
"target_modules": ["q_proj"],
"bias": "none",
"task_type": "CAUSAL_LM",
"base_model_name_or_path": "old-base",
},
)
summary = converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
lora_alpha=16,
)
out = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
cleaned_key = "model.layers.0.self_attn.q_proj.lora_A.weight"
assert cleaned_key in out
torch.testing.assert_close(out[cleaned_key], existing_tensor)
assert summary["lora_alpha"] == 9
config = converter._load_json(output_dir / converter.ADAPTER_CONFIG_FILE)
assert config["lora_alpha"] == 9
assert config["base_model_name_or_path"] == "/models/base"
assert config["target_modules"] == ["q_proj", "gate_proj", "up_proj", "down_proj"]
def test_writes_split_expert_and_nonexpert_adapters(tmp_path):
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
expert_dir = tmp_path / "expert"
nonexpert_dir = tmp_path / "nonexpert"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir)
q_proj_tensor = torch.ones(3, 5)
o_proj_tensor = torch.full((5, 3), 2.0)
save_file(
{
"base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": q_proj_tensor,
"base_model.model.model.layers.0.self_attn.o_proj.lora_B.weight": o_proj_tensor,
},
str(input_dir / converter.ADAPTER_MODEL_FILE),
)
converter._write_json(
input_dir / converter.ADAPTER_CONFIG_FILE,
{
"peft_type": "LORA",
"r": 3,
"lora_alpha": 9,
"target_modules": ["q_proj", "o_proj"],
"bias": "none",
"task_type": "CAUSAL_LM",
"base_model_name_or_path": "old-base",
},
)
summary = converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
expert_output_dir=expert_dir,
nonexpert_output_dir=nonexpert_dir,
)
merged = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
expert = load_file(str(expert_dir / converter.ADAPTER_MODEL_FILE))
nonexpert = load_file(str(nonexpert_dir / converter.ADAPTER_MODEL_FILE))
assert summary["tensor_count"] == 14
assert summary["split_outputs"]["expert"]["tensor_count"] == 12
assert summary["split_outputs"]["nonexpert"]["tensor_count"] == 2
assert set(merged) == set(expert) | set(nonexpert)
assert set(expert).isdisjoint(nonexpert)
assert all(".mlp.experts." in key for key in expert)
assert not any(".mlp.experts." in key for key in nonexpert)
cleaned_q_proj_key = "model.layers.0.self_attn.q_proj.lora_A.weight"
cleaned_o_proj_key = "model.layers.0.self_attn.o_proj.lora_B.weight"
torch.testing.assert_close(nonexpert[cleaned_q_proj_key], q_proj_tensor)
torch.testing.assert_close(nonexpert[cleaned_o_proj_key], o_proj_tensor)
expert_config = converter._load_json(expert_dir / converter.ADAPTER_CONFIG_FILE)
nonexpert_config = converter._load_json(nonexpert_dir / converter.ADAPTER_CONFIG_FILE)
assert expert_config["target_modules"] == ["gate_proj", "up_proj", "down_proj"]
assert nonexpert_config["target_modules"] == ["q_proj", "o_proj"]
assert expert_config["base_model_name_or_path"] == "/models/base"
assert nonexpert_config["base_model_name_or_path"] == "/models/base"
def test_requires_lora_alpha_without_input_config(tmp_path):
input_dir = tmp_path / "input"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir)
with pytest.raises(ValueError, match="pass --lora-alpha"):
converter.convert_kt_to_sglang_adapter(
input_dir,
tmp_path / "output",
base_model_name_or_path="/models/base",
)
def test_rejects_inconsistent_rank(tmp_path):
input_dir = tmp_path / "input"
input_dir.mkdir()
save_file(
{
"layers.0.experts.gate_lora_a": torch.zeros(2, 3, 5),
"layers.0.experts.gate_lora_b": torch.zeros(2, 7, 4),
},
str(input_dir / converter.FUSED_EXPERT_LORA_FILE),
)
with pytest.raises(ValueError, match="Inconsistent LoRA ranks"):
converter.convert_kt_to_sglang_adapter(
input_dir,
tmp_path / "output",
base_model_name_or_path="/models/base",
lora_alpha=8,
)
def test_rejects_unexpected_fused_key(tmp_path):
input_dir = tmp_path / "input"
input_dir.mkdir()
save_file(
{"layers.0.experts.unknown_lora_a": torch.zeros(2, 3, 5)},
str(input_dir / converter.FUSED_EXPERT_LORA_FILE),
)
with pytest.raises(ValueError, match="Unsupported KT fused expert LoRA tensor"):
converter.convert_kt_to_sglang_adapter(
input_dir,
tmp_path / "output",
base_model_name_or_path="/models/base",
lora_alpha=8,
)
def test_rejects_nonempty_output_without_overwrite(tmp_path):
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
input_dir.mkdir()
output_dir.mkdir()
(output_dir / "existing.txt").write_text("do not remove", encoding="utf-8")
_write_full_fused_checkpoint(input_dir)
with pytest.raises(FileExistsError, match="Output directory is not empty"):
converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
lora_alpha=8,
)
converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
lora_alpha=8,
overwrite=True,
)
assert not (output_dir / "existing.txt").exists()
assert (output_dir / converter.ADAPTER_MODEL_FILE).exists()
def test_rejects_output_same_as_input(tmp_path):
input_dir = tmp_path / "input"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir)
with pytest.raises(ValueError, match="different from input"):
converter.convert_kt_to_sglang_adapter(
input_dir,
input_dir,
base_model_name_or_path="/models/base",
lora_alpha=8,
overwrite=True,
)
def test_rejects_output_ancestor_of_input(tmp_path):
run_dir = tmp_path / "run"
input_dir = run_dir / "adapter"
output_dir = run_dir
input_dir.mkdir(parents=True)
_write_full_fused_checkpoint(input_dir)
with pytest.raises(ValueError, match="ancestor/descendant"):
converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
lora_alpha=8,
overwrite=True,
)
def test_rejects_output_descendant_of_input(tmp_path):
input_dir = tmp_path / "input"
output_dir = input_dir / "output"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir)
with pytest.raises(ValueError, match="ancestor/descendant"):
converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
lora_alpha=8,
overwrite=True,
)
def test_rejects_split_output_ancestor_relationship(tmp_path):
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
expert_dir = tmp_path / "split" / "expert"
nonexpert_dir = tmp_path / "split"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir)
save_file(
{
"base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": torch.ones(3, 5),
},
str(input_dir / converter.ADAPTER_MODEL_FILE),
)
converter._write_json(
input_dir / converter.ADAPTER_CONFIG_FILE,
{
"peft_type": "LORA",
"r": 3,
"lora_alpha": 9,
"target_modules": ["q_proj"],
"bias": "none",
"task_type": "CAUSAL_LM",
"base_model_name_or_path": "old-base",
},
)
with pytest.raises(ValueError, match="ancestor/descendant"):
converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
expert_output_dir=expert_dir,
nonexpert_output_dir=nonexpert_dir,
)
def test_rejects_mismatched_nonexpert_rank(tmp_path):
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
input_dir.mkdir()
_write_full_fused_checkpoint(input_dir, rank=3)
save_file(
{
"base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": torch.ones(4, 5),
},
str(input_dir / converter.ADAPTER_MODEL_FILE),
)
converter._write_json(
input_dir / converter.ADAPTER_CONFIG_FILE,
{
"peft_type": "LORA",
"r": 4,
"lora_alpha": 9,
"target_modules": ["q_proj"],
"bias": "none",
"task_type": "CAUSAL_LM",
"base_model_name_or_path": "old-base",
},
)
with pytest.raises(ValueError, match="rank mismatch"):
converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path="/models/base",
)
@@ -0,0 +1,242 @@
import importlib.util
import json
import os
import re
import shutil
import sys
import time
from pathlib import Path
import pytest
import torch
from safetensors.torch import load_file
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=60, suite="default")
KT_ADAPTER_ENV = "KT_LORA_ADAPTER_DIR"
KT_BASE_MODEL_ENV = "KT_LORA_BASE_MODEL"
KT_ALPHA_ENV = "KT_LORA_ALPHA"
KT_LARGE_ADAPTER_ENV = "KT_LORA_LARGE_ADAPTER_DIR"
REPO_ROOT = Path(__file__).resolve().parents[3]
SCRIPT_PATH = (
Path(__file__).resolve().parents[2]
/ "scripts"
/ "convert_kt_to_sglang_adapter.py"
)
SPEC = importlib.util.spec_from_file_location("convert_kt_to_sglang_adapter", SCRIPT_PATH)
converter = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(converter)
SGLANG_EXPERT_KEY_RE = re.compile(
r"^model\.layers\.(\d+)\.mlp\.experts\.(\d+)\."
r"(gate_proj|up_proj|down_proj)\.lora_[AB]\.weight$"
)
def _required_adapter_dir(env_name: str) -> Path:
value = os.environ.get(env_name)
if not value:
pytest.skip(f"Set {env_name} to run real adapter integration tests.")
path = Path(value).expanduser().resolve()
if not path.is_dir():
pytest.fail(f"{env_name} is not a directory: {path}")
if not (path / converter.FUSED_EXPERT_LORA_FILE).is_file():
pytest.fail(f"{env_name} must contain {converter.FUSED_EXPERT_LORA_FILE}: {path}")
return path
def _base_model_name_or_path() -> str:
value = os.environ.get(KT_BASE_MODEL_ENV)
if not value:
pytest.fail(f"Set {KT_BASE_MODEL_ENV} before running real adapter integration tests.")
return value
def _optional_lora_alpha() -> float | None:
value = os.environ.get(KT_ALPHA_ENV)
if value in (None, ""):
return None
try:
return float(value)
except ValueError:
pytest.fail(f"{KT_ALPHA_ENV} must be numeric, got: {value!r}")
def _lora_alpha_for_input(input_dir: Path) -> float | None:
alpha = _optional_lora_alpha()
if (input_dir / converter.ADAPTER_CONFIG_FILE).exists():
return alpha
if alpha is None:
pytest.fail(
f"{input_dir} has no {converter.ADAPTER_CONFIG_FILE}; set {KT_ALPHA_ENV}."
)
return alpha
def _convert_real_adapter(input_dir: Path, tmp_path: Path, output_name: str = "output") -> tuple[Path, dict]:
output_dir = tmp_path / output_name
summary = converter.convert_kt_to_sglang_adapter(
input_dir,
output_dir,
base_model_name_or_path=_base_model_name_or_path(),
lora_alpha=_lora_alpha_for_input(input_dir),
)
return output_dir, summary
def _load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def _link_or_copy(source: Path, dest: Path) -> None:
try:
os.symlink(source, dest)
except OSError:
shutil.copy2(source, dest)
def _assert_config_shape(output_dir: Path, summary: dict) -> dict:
config = _load_json(output_dir / converter.ADAPTER_CONFIG_FILE)
assert config["peft_type"] == "LORA"
assert config["r"] == summary["rank"]
assert config["lora_alpha"] == summary["lora_alpha"]
assert config["base_model_name_or_path"] == _base_model_name_or_path()
assert {"gate_proj", "up_proj", "down_proj"}.issubset(config["target_modules"])
return config
def _assert_fused_tensors_preserved(input_dir: Path, output_dir: Path) -> int:
fused_tensors = load_file(str(input_dir / converter.FUSED_EXPERT_LORA_FILE))
output_tensors = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
checked = 0
for input_key, input_tensor in sorted(fused_tensors.items()):
match = converter.KT_FUSED_KEY_RE.match(input_key)
assert match is not None, input_key
layer_idx, kt_name = match.groups()
assert kt_name in converter.KT_NAME_MAP, input_key
assert input_tensor.dim() == 3, input_key
proj_name, lora_name, _rank_dim = converter.KT_NAME_MAP[kt_name]
for expert_idx in range(input_tensor.shape[0]):
output_key = (
f"model.layers.{layer_idx}.mlp.experts.{expert_idx}."
f"{proj_name}.{lora_name}.weight"
)
assert SGLANG_EXPERT_KEY_RE.match(output_key), output_key
assert output_key in output_tensors
assert output_tensors[output_key].shape == input_tensor[expert_idx].shape
assert output_tensors[output_key].dtype == input_tensor.dtype
assert torch.equal(output_tensors[output_key], input_tensor[expert_idx].cpu())
checked += 1
assert checked > 0
return checked
@pytest.mark.requires_model
def test_real_adapter_conversion_preserves_fused_tensors_and_config(tmp_path):
input_dir = _required_adapter_dir(KT_ADAPTER_ENV)
output_dir, summary = _convert_real_adapter(input_dir, tmp_path)
checked = _assert_fused_tensors_preserved(input_dir, output_dir)
config = _assert_config_shape(output_dir, summary)
output_tensors = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
assert summary["tensor_count"] == len(output_tensors)
assert checked <= summary["tensor_count"]
assert config["target_modules"] == summary["target_modules"]
@pytest.mark.requires_model
def test_real_adapter_directory_merges_existing_adapter_model(tmp_path):
input_dir = _required_adapter_dir(KT_ADAPTER_ENV)
existing_adapter_path = input_dir / converter.ADAPTER_MODEL_FILE
if not existing_adapter_path.exists():
pytest.skip(f"{input_dir} has no {converter.ADAPTER_MODEL_FILE} to merge.")
output_dir, _summary = _convert_real_adapter(input_dir, tmp_path)
input_tensors = load_file(str(existing_adapter_path))
output_tensors = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
for input_key, input_tensor in input_tensors.items():
output_key = converter._clean_adapter_key(input_key)
assert output_key in output_tensors
assert output_tensors[output_key].shape == input_tensor.shape
assert output_tensors[output_key].dtype == input_tensor.dtype
assert torch.equal(output_tensors[output_key], input_tensor.cpu())
@pytest.mark.requires_model
def test_real_fused_conversion_without_input_config_uses_env_alpha(tmp_path):
input_dir = _required_adapter_dir(KT_ADAPTER_ENV)
alpha = _optional_lora_alpha()
if alpha is None:
pytest.skip(f"Set {KT_ALPHA_ENV} to validate conversion without input config.")
no_config_input = tmp_path / "input_without_config"
no_config_input.mkdir()
_link_or_copy(
input_dir / converter.FUSED_EXPERT_LORA_FILE,
no_config_input / converter.FUSED_EXPERT_LORA_FILE,
)
existing_adapter_path = input_dir / converter.ADAPTER_MODEL_FILE
if existing_adapter_path.exists():
_link_or_copy(existing_adapter_path, no_config_input / converter.ADAPTER_MODEL_FILE)
output_dir, summary = _convert_real_adapter(no_config_input, tmp_path, "output_without_config")
assert summary["lora_alpha"] == alpha
config = _assert_config_shape(output_dir, summary)
assert config["lora_alpha"] == alpha
_assert_fused_tensors_preserved(no_config_input, output_dir)
@pytest.mark.requires_model
def test_sglang_lora_config_loader_accepts_converted_adapter(tmp_path):
input_dir = _required_adapter_dir(KT_ADAPTER_ENV)
output_dir, summary = _convert_real_adapter(input_dir, tmp_path)
sglang_python = REPO_ROOT / "third_party" / "sglang" / "python"
sys.path.insert(0, str(sglang_python))
try:
from sglang.srt.lora.lora_config import LoRAConfig
except Exception as exc:
pytest.fail(f"Unable to import SGLang LoRAConfig: {exc}")
lora_config = LoRAConfig(str(output_dir))
output_tensors = load_file(str(output_dir / converter.ADAPTER_MODEL_FILE))
assert lora_config.r == summary["rank"]
assert lora_config.lora_alpha == summary["lora_alpha"]
assert lora_config.target_modules == summary["target_modules"]
assert len(output_tensors) == summary["tensor_count"]
@pytest.mark.requires_model
def test_large_adapter_conversion_smoke(tmp_path, record_property):
input_dir = _required_adapter_dir(KT_LARGE_ADAPTER_ENV)
start_time = time.perf_counter()
output_dir, summary = _convert_real_adapter(input_dir, tmp_path, "large_output")
duration_seconds = time.perf_counter() - start_time
output_path = output_dir / converter.ADAPTER_MODEL_FILE
output_tensors = load_file(str(output_path))
config = _assert_config_shape(output_dir, summary)
record_property("conversion_seconds", round(duration_seconds, 3))
record_property("output_bytes", output_path.stat().st_size)
record_property("tensor_count", summary["tensor_count"])
assert len(output_tensors) == summary["tensor_count"]
assert config["target_modules"] == summary["target_modules"]
@@ -0,0 +1,36 @@
"""CUDA backend tests for KT-Kernel (Placeholder).
This file is a placeholder for future CUDA backend tests.
Currently, KT-Kernel focuses on CPU optimizations (Intel AMX/AVX512).
To implement CUDA tests:
1. Add actual test functions with @pytest.mark.cuda
2. Update the estimated time in register_cuda_ci()
3. Implement CUDA-specific initialization and validation tests
"""
import os
import sys
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cuda_ci
# Register this test for CUDA CI (estimated time: 10 seconds, placeholder)
# Update suite name when implementing: currently using "stage-a-test-1"
register_cuda_ci(est_time=10, suite="stage-a-test-1")
def test_cuda_placeholder():
"""Placeholder test for CUDA backend.
TODO: Implement actual CUDA tests when CUDA support is added to kt-kernel.
"""
# Currently a no-op placeholder
pass
if __name__ == "__main__":
# Allow running standalone (required by test runner)
print("⚠ CUDA tests are not yet implemented (placeholder)")
print("✓ Placeholder test passed")
@@ -0,0 +1,91 @@
"""Regression tests for SafeTensorLoader.load_experts expert-count guard.
After the discovery loop, ``max_experts_count`` holds the highest expert index
found, i.e. ``(expert count - 1)``. It is ``-1`` only when no experts exist for
the key, so the guard must reject ``-1`` (no experts), not ``0`` (exactly one
expert). The previous ``== 0`` check was an off-by-one that falsely raised
"No experts found" for a single-expert layer and silently returned empty weight
lists for the genuine zero-expert case.
The loader module is imported as a top-level module (its directory is added to
sys.path) so the test does not pull in the compiled kt_kernel_ext extension via
utils/__init__.py.
"""
import os
import sys
import unittest
# Register this test for CPU CI.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="default")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "utils"))
import loader
SafeTensorLoader = loader.SafeTensorLoader
class _FakeTensor:
"""Stand-in for a loaded tensor; load_experts only calls ``.numpy()``."""
def numpy(self):
return 0
class _StubSafeTensorLoader(SafeTensorLoader):
"""SafeTensorLoader backed by an explicit key set instead of real files."""
def __init__(self, keys):
# Bypass the filesystem/safetensors scan in the real __init__.
self.tensor_file_map = {k: "stub.safetensors" for k in keys}
self.file_handle_map = {}
self.tensor_type_map = {}
self.tensor_device_map = {}
def load_tensor(self, key: str, device: str = "cpu"):
assert key in self.tensor_file_map, f"unexpected key requested: {key}"
return _FakeTensor()
def _expert_keys(base_key, n_experts, n_numa=1):
"""Build the NUMA-sharded expert key set that load_experts expects."""
keys = []
for proj in ("ffn_up_exps", "ffn_gate_exps", "ffn_down_exps"):
for expert in range(n_experts):
for numa in range(n_numa):
prefix = f"{base_key}.{proj}.{expert}.numa.{numa}"
keys.append(f"{prefix}.weight")
keys.append(f"{prefix}.scale")
return keys
class TestLoadExpertsCountGuard(unittest.TestCase):
def test_single_expert_is_loaded(self):
"""A layer with exactly one expert must load, not raise."""
loader = _StubSafeTensorLoader(_expert_keys("blk.0", n_experts=1))
result = loader.load_experts("blk.0")
for proj in ("up", "gate", "down"):
self.assertEqual(len(result[proj]), 1, f"{proj}: expected 1 numa group")
self.assertEqual(len(result[proj][0]), 1, f"{proj}: expected 1 expert")
def test_zero_experts_raises(self):
"""No experts under the key must raise, not silently return empties."""
loader = _StubSafeTensorLoader(["blk.0.attn_norm.weight"])
with self.assertRaises(ValueError):
loader.load_experts("blk.0")
def test_multiple_experts_and_numa_counts(self):
"""Counts are correct across several experts and NUMA shards."""
loader = _StubSafeTensorLoader(_expert_keys("blk.0", n_experts=3, n_numa=2))
result = loader.load_experts("blk.0")
for proj in ("up", "gate", "down"):
self.assertEqual(len(result[proj]), 2, f"{proj}: expected 2 numa groups")
for numa_group in result[proj]:
self.assertEqual(len(numa_group), 3, f"{proj}: expected 3 experts")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,205 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT4 accuracy tests for KT-Kernel.
Tests accuracy of AMX-accelerated INT4 MOE operations against torch reference.
"""
import os
import sys
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 120 seconds
register_cpu_ci(est_time=120, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original test_moe_amx.py)
expert_num = 256
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
qlen = 1
layer_num = 1
validation_iter = 2
physical_to_logical_map = None
def act_fn(x):
"""Activation function for MoE."""
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MLP."""
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
ret = torch.mm(intermediate, down_proj.t())
return ret
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MoE."""
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
@pytest.mark.cpu
def test_moe_amx_int4_accuracy():
"""Test AMX INT4 MOE accuracy against PyTorch reference implementation."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
global physical_to_logical_map
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
CPUInfer = kt_kernel_ext.CPUInfer(60)
with torch.inference_mode(mode=True):
# Initialize MoE layers
gate_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn(
(expert_num, hidden_size, intermediate_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
# Create MOE config
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.gate_scale = 0
config.pool = CPUInfer.backend_
# Initialize INT4 MOE
moe = kt_kernel_ext.moe.AMXInt4_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
CPUInfer.submit(moe.warm_up_task())
CPUInfer.sync()
# Run validation iterations
for i in range(validation_iter):
bsz_tensor = torch.tensor([qlen], device="cpu")
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = torch.randn((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
input_data = input_data / 100
# Run AMX MOE
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Run torch reference
t_output = moe_torch(input_data, expert_ids, weights, gate_proj, up_proj, down_proj)
# Calculate relative difference
diff = torch.mean(torch.abs(output - t_output)) / torch.mean(torch.abs(t_output))
print(f"Iteration {i}, diff = {diff:.6f}")
# INT4 should have diff < 0.35
assert diff < 0.35, f"INT4 accuracy test failed: diff={diff:.6f} >= 0.35"
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"⚠ Dependencies not available: {import_error}")
print("Skipping AMX MOE INT4 accuracy tests")
return
try:
print("Running AMX MOE INT4 accuracy test...")
test_moe_amx_int4_accuracy()
print("✓ AMX MOE INT4 accuracy test passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,205 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT4_1 accuracy tests for KT-Kernel.
Tests accuracy of AMX-accelerated INT4_1 MOE operations against torch reference.
"""
import os
import sys
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 120 seconds
register_cpu_ci(est_time=120, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original test_moe_amx.py)
expert_num = 256
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
qlen = 1
layer_num = 1
validation_iter = 2
physical_to_logical_map = None
def act_fn(x):
"""Activation function for MoE."""
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MLP."""
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
ret = torch.mm(intermediate, down_proj.t())
return ret
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MoE."""
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
@pytest.mark.cpu
def test_moe_amx_int4_1_accuracy():
"""Test AMX INT4_1 MOE accuracy against PyTorch reference implementation."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
global physical_to_logical_map
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
CPUInfer = kt_kernel_ext.CPUInfer(60)
with torch.inference_mode(mode=True):
# Initialize MoE layers
gate_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn(
(expert_num, hidden_size, intermediate_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
# Create MOE config
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.gate_scale = 0
config.pool = CPUInfer.backend_
# Initialize INT4_1 MOE
moe = kt_kernel_ext.moe.AMXInt4_1_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
CPUInfer.submit(moe.warm_up_task())
CPUInfer.sync()
# Run validation iterations
for i in range(validation_iter):
bsz_tensor = torch.tensor([qlen], device="cpu")
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = torch.randn((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
input_data = input_data / 100
# Run AMX MOE
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Run torch reference
t_output = moe_torch(input_data, expert_ids, weights, gate_proj, up_proj, down_proj)
# Calculate relative difference
diff = torch.mean(torch.abs(output - t_output)) / torch.mean(torch.abs(t_output))
print(f"Iteration {i}, diff = {diff:.6f}")
# INT4_1 should have diff < 0.35
assert diff < 0.35, f"INT4_1 accuracy test failed: diff={diff:.6f} >= 0.35"
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"⚠ Dependencies not available: {import_error}")
print("Skipping AMX MOE INT4_1 accuracy tests")
return
try:
print("Running AMX MOE INT4_1 accuracy test...")
test_moe_amx_int4_1_accuracy()
print("✓ AMX MOE INT4_1 accuracy test passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,209 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT4_1K accuracy tests for KT-Kernel.
Tests accuracy of AMX-accelerated INT4_1K group quantization MOE operations against torch reference.
"""
import os
import sys
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 120 seconds
register_cpu_ci(est_time=120, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original test_moe_amx.py)
expert_num = 256
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
qlen = 1
layer_num = 1
validation_iter = 2
k_group_size = 64
physical_to_logical_map = None
def act_fn(x):
"""Activation function for MoE."""
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MLP."""
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
ret = torch.mm(intermediate, down_proj.t())
return ret
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MoE."""
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
@pytest.mark.cpu
def test_moe_amx_int4_1k_accuracy():
"""Test AMX INT4_1K MOE accuracy against PyTorch reference implementation."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
global physical_to_logical_map
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
CPUInfer = kt_kernel_ext.CPUInfer(60)
with torch.inference_mode(mode=True):
# Initialize MoE layers
gate_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn(
(expert_num, hidden_size, intermediate_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
# Create MOE config
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.gate_scale = 0
config.pool = CPUInfer.backend_
# Configure INT4_1K quantization settings
config.quant_config.bits = 4
config.quant_config.group_size = k_group_size
config.quant_config.zero_point = True
# Initialize INT4_1K MOE
moe = kt_kernel_ext.moe.AMXInt4_1KGroup_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
# Run validation iterations
for i in range(validation_iter):
bsz_tensor = torch.tensor([qlen], device="cpu")
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = torch.randn((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
input_data = input_data / 100
# Run AMX MOE
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Run torch reference
t_output = moe_torch(input_data, expert_ids, weights, gate_proj, up_proj, down_proj)
# Calculate relative difference
diff = torch.mean(torch.abs(output - t_output)) / torch.mean(torch.abs(t_output))
print(f"Iteration {i}, diff = {diff:.6f}")
# INT4_1K should have diff < 0.35
assert diff < 0.35, f"INT4_1K accuracy test failed: diff={diff:.6f} >= 0.35"
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"⚠ Dependencies not available: {import_error}")
print("Skipping AMX MOE INT4_1K accuracy tests")
return
try:
print("Running AMX MOE INT4_1K accuracy test...")
test_moe_amx_int4_1k_accuracy()
print("✓ AMX MOE INT4_1K accuracy test passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,203 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT8 accuracy tests for KT-Kernel.
Tests accuracy of AMX-accelerated INT8 MOE operations against torch reference.
"""
import os
import sys
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 120 seconds
register_cpu_ci(est_time=120, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original test_moe_amx.py)
expert_num = 256
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
qlen = 1
layer_num = 1
validation_iter = 2
physical_to_logical_map = None
def act_fn(x):
"""Activation function for MoE."""
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MLP."""
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
ret = torch.mm(intermediate, down_proj.t())
return ret
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
"""PyTorch reference implementation of MoE."""
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
@pytest.mark.cpu
def test_moe_amx_int8_accuracy():
"""Test AMX INT8 MOE accuracy against PyTorch reference implementation."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
global physical_to_logical_map
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
CPUInfer = kt_kernel_ext.CPUInfer(60)
with torch.inference_mode(mode=True):
# Initialize MoE layers
gate_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn(
(expert_num, intermediate_size, hidden_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn(
(expert_num, hidden_size, intermediate_size),
dtype=torch.bfloat16,
device="cuda",
)
.to("cpu")
.contiguous()
)
# Create MOE config
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.gate_scale = 0
config.pool = CPUInfer.backend_
# Initialize INT8 MOE
moe = kt_kernel_ext.moe.AMXInt8_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
# Run validation iterations
for i in range(validation_iter):
bsz_tensor = torch.tensor([qlen], device="cpu")
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = torch.randn((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
input_data = input_data / 100
# Run AMX MOE
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Run torch reference
t_output = moe_torch(input_data, expert_ids, weights, gate_proj, up_proj, down_proj)
# Calculate relative difference
diff = torch.mean(torch.abs(output - t_output)) / torch.mean(torch.abs(t_output))
print(f"Iteration {i}, diff = {diff:.6f}")
# INT8 should have diff < 0.05
assert diff < 0.05, f"INT8 accuracy test failed: diff={diff:.6f} >= 0.05"
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"⚠ Dependencies not available: {import_error}")
print("Skipping AMX MOE INT8 accuracy tests")
return
try:
print("Running AMX MOE INT8 accuracy test...")
test_moe_amx_int8_accuracy()
print("✓ AMX MOE INT8 accuracy test passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,317 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT4 benchmark tests for KT-Kernel.
Benchmarks performance (bandwidth and FLOPS) of AMX-accelerated INT4 MOE operations.
"""
import os
import sys
import time
import json
import subprocess
import platform
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 300 seconds
register_cpu_ci(est_time=300, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
from tqdm import tqdm
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original bench_moe_amx.py)
expert_num = 16
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
layer_num = 2
qlen = 2048
warm_up_iter = 1000
test_iter = 2000
# Worker configuration
worker_config_dict = {
"subpool_count": 2,
"subpool_numa_map": [0, 1],
"subpool_thread_count": [30, 30],
}
CPUINFER_PARAM = 60
def get_git_commit():
"""Get current git commit information."""
result = {}
try:
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip()
commit_msg = subprocess.check_output(["git", "log", "-1", "--pretty=%B"]).decode("utf-8").strip()
result["commit"] = commit
result["commit_message"] = commit_msg
dirty_output = subprocess.check_output(["git", "status", "--porcelain"]).decode("utf-8").strip()
if dirty_output:
result["dirty"] = True
result["dirty_files"] = dirty_output.splitlines()
else:
result["dirty"] = False
except Exception as e:
result["commit"] = None
result["commit_message"] = None
result["dirty"] = None
result["error"] = str(e)
return result
def get_system_info():
"""Get system information including CPU model, memory, cores, and sockets."""
info = {}
uname = platform.uname()
info["system_name"] = uname.system
info["node_name"] = uname.node
# Get CPU model (Linux only)
cpu_model = None
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line:
cpu_model = line.split(":", 1)[1].strip()
break
except Exception as e:
cpu_model = f"Error: {e}"
info["cpu_model"] = cpu_model
# Get memory size in GB (Linux only)
mem_total_gb = None
if os.path.exists("/proc/meminfo"):
try:
with open("/proc/meminfo", "r") as f:
for line in f:
if "MemTotal" in line:
mem_kb = float(line.split(":", 1)[1].split()[0])
mem_total_gb = round(mem_kb / (1024 * 1024), 2)
break
except Exception as e:
mem_total_gb = f"Error: {e}"
info["memory_size_GB"] = mem_total_gb
# Get CPU core count
info["cpu_core_count"] = os.cpu_count()
# Get socket count
sockets = set()
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "physical id" in line:
sockets.add(line.split(":", 1)[1].strip())
except Exception as e:
sockets = set()
info["cpu_socket_count"] = len(sockets) if len(sockets) > 0 else 1
return info
def record_results(result, filename):
"""Append results to JSONL file."""
with open(filename, "a") as f:
f.write(json.dumps(result) + "\n")
@pytest.mark.cpu
def test_moe_amx_int4_benchmark():
"""Benchmark AMX INT4 MOE performance."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
quant_mode = "int4"
bytes_per_elem = 0.5
# Setup output file
script_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(script_dir, "bench_moe_amx_int4.jsonl")
with torch.inference_mode():
# Initialize CPUInfer with worker config
worker_config = kt_kernel_ext.WorkerPoolConfig()
worker_config.subpool_count = worker_config_dict["subpool_count"]
worker_config.subpool_numa_map = worker_config_dict["subpool_numa_map"]
worker_config.subpool_thread_count = worker_config_dict["subpool_thread_count"]
CPUInfer = kt_kernel_ext.CPUInfer(worker_config)
# Initialize MOE layers
moes = []
for layer_index in range(layer_num):
gate_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.pool = CPUInfer.backend_
moe = kt_kernel_ext.moe.AMXInt4_MOE(config)
CPUInfer.submit(moe.load_weights_task())
CPUInfer.sync()
moes.append(moe)
# Generate test data
gen_iter = 3000
expert_ids = (
torch.rand(gen_iter * qlen, expert_num, device="cpu")
.argsort(dim=-1)[:, :num_experts_per_tok]
.reshape(gen_iter, qlen * num_experts_per_tok)
.to("cpu")
.contiguous()
)
weights = (
torch.rand((gen_iter, qlen, num_experts_per_tok), dtype=torch.float32, device="cpu").to("cpu").contiguous()
)
input_tensor = (
torch.randn((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
output_tensor = (
torch.empty((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
bsz_tensor = torch.tensor([qlen], device="cpu")
# Warm-up iterations
print(f"Running warm-up for {warm_up_iter} iterations...")
for i in tqdm(range(warm_up_iter), desc="Warm-up"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
# Test iterations
print(f"Running test for {test_iter} iterations...")
start = time.perf_counter()
for i in tqdm(range(test_iter), desc="Testing"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
end = time.perf_counter()
total_time = end - start
# Calculate performance metrics
time_per_iter_us = total_time / test_iter * 1e6
bandwidth = (
hidden_size
* intermediate_size
* 3
* num_experts_per_tok
* (1 / 8 * 256 * (1 - (31 / 32) ** qlen))
* bytes_per_elem
* test_iter
/ total_time
/ 1e9
) # GB/s
flops = (
hidden_size * intermediate_size * qlen * 3 * num_experts_per_tok * 2 * test_iter / total_time / 1e12
) # TFLOPS
print("Quant mode: ", quant_mode)
print("Time(s): ", total_time)
print("Iteration: ", test_iter)
print("Time(us) per iteration: ", time_per_iter_us)
print("Bandwidth: ", bandwidth, "GB/s")
print("Flops: ", flops, "TFLOPS")
# Record results
result = {
"quant_mode": quant_mode,
"total_time_seconds": total_time,
"iterations": test_iter,
"time_per_iteration_us": time_per_iter_us,
"bandwidth_GBs": bandwidth,
"flops_TFLOPS": flops,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"test_parameters": {
"expert_num": expert_num,
"hidden_size": hidden_size,
"intermediate_size": intermediate_size,
"max_len": max_len,
"num_experts_per_tok": num_experts_per_tok,
"layer_num": layer_num,
"qlen": qlen,
"warm_up_iter": warm_up_iter,
"test_iter": test_iter,
"CPUInfer_parameter": CPUINFER_PARAM,
},
}
result.update(get_git_commit())
result.update(get_system_info())
record_results(result, json_path)
print(f"Results saved to {json_path}")
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"⚠ Dependencies not available: {import_error}")
print("Skipping AMX MOE INT4 benchmark tests")
return
try:
print("Running AMX MOE INT4 benchmark test...")
test_moe_amx_int4_benchmark()
print("✓ AMX MOE INT4 benchmark test passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,317 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT4 benchmark tests for KT-Kernel.
Benchmarks performance (bandwidth and FLOPS) of AMX-accelerated INT4 MOE operations.
"""
import os
import sys
import time
import json
import subprocess
import platform
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 300 seconds
register_cpu_ci(est_time=300, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
from tqdm import tqdm
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original bench_moe_amx.py)
expert_num = 16
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
layer_num = 2
qlen = 1024
warm_up_iter = 1000
test_iter = 2000
# Worker configuration
worker_config_dict = {
"subpool_count": 2,
"subpool_numa_map": [0, 1],
"subpool_thread_count": [30, 30],
}
CPUINFER_PARAM = 60
def get_git_commit():
"""Get current git commit information."""
result = {}
try:
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip()
commit_msg = subprocess.check_output(["git", "log", "-1", "--pretty=%B"]).decode("utf-8").strip()
result["commit"] = commit
result["commit_message"] = commit_msg
dirty_output = subprocess.check_output(["git", "status", "--porcelain"]).decode("utf-8").strip()
if dirty_output:
result["dirty"] = True
result["dirty_files"] = dirty_output.splitlines()
else:
result["dirty"] = False
except Exception as e:
result["commit"] = None
result["commit_message"] = None
result["dirty"] = None
result["error"] = str(e)
return result
def get_system_info():
"""Get system information including CPU model, memory, cores, and sockets."""
info = {}
uname = platform.uname()
info["system_name"] = uname.system
info["node_name"] = uname.node
# Get CPU model (Linux only)
cpu_model = None
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line:
cpu_model = line.split(":", 1)[1].strip()
break
except Exception as e:
cpu_model = f"Error: {e}"
info["cpu_model"] = cpu_model
# Get memory size in GB (Linux only)
mem_total_gb = None
if os.path.exists("/proc/meminfo"):
try:
with open("/proc/meminfo", "r") as f:
for line in f:
if "MemTotal" in line:
mem_kb = float(line.split(":", 1)[1].split()[0])
mem_total_gb = round(mem_kb / (1024 * 1024), 2)
break
except Exception as e:
mem_total_gb = f"Error: {e}"
info["memory_size_GB"] = mem_total_gb
# Get CPU core count
info["cpu_core_count"] = os.cpu_count()
# Get socket count
sockets = set()
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "physical id" in line:
sockets.add(line.split(":", 1)[1].strip())
except Exception as e:
sockets = set()
info["cpu_socket_count"] = len(sockets) if len(sockets) > 0 else 1
return info
def record_results(result, filename):
"""Append results to JSONL file."""
with open(filename, "a") as f:
f.write(json.dumps(result) + "\n")
@pytest.mark.cpu
def test_moe_amx_int4_1_benchmark():
"""Benchmark AMX INT4 MOE performance."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
quant_mode = "int4"
bytes_per_elem = 0.5
# Setup output file
script_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(script_dir, "bench_moe_amx_int4_1.jsonl")
with torch.inference_mode():
# Initialize CPUInfer with worker config
worker_config = kt_kernel_ext.WorkerPoolConfig()
worker_config.subpool_count = worker_config_dict["subpool_count"]
worker_config.subpool_numa_map = worker_config_dict["subpool_numa_map"]
worker_config.subpool_thread_count = worker_config_dict["subpool_thread_count"]
CPUInfer = kt_kernel_ext.CPUInfer(worker_config)
# Initialize MOE layers
moes = []
for layer_index in range(layer_num):
gate_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.pool = CPUInfer.backend_
moe = kt_kernel_ext.moe.AMXInt4_MOE(config)
CPUInfer.submit(moe.load_weights_task())
CPUInfer.sync()
moes.append(moe)
# Generate test data
gen_iter = 3000
expert_ids = (
torch.rand(gen_iter * qlen, expert_num, device="cpu")
.argsort(dim=-1)[:, :num_experts_per_tok]
.reshape(gen_iter, qlen * num_experts_per_tok)
.to("cpu")
.contiguous()
)
weights = (
torch.rand((gen_iter, qlen, num_experts_per_tok), dtype=torch.float32, device="cpu").to("cpu").contiguous()
)
input_tensor = (
torch.randn((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
output_tensor = (
torch.empty((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
bsz_tensor = torch.tensor([qlen], device="cpu")
# Warm-up iterations
print(f"Running warm-up for {warm_up_iter} iterations...")
for i in tqdm(range(warm_up_iter), desc="Warm-up"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
# Test iterations
print(f"Running test for {test_iter} iterations...")
start = time.perf_counter()
for i in tqdm(range(test_iter), desc="Testing"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
end = time.perf_counter()
total_time = end - start
# Calculate performance metrics
time_per_iter_us = total_time / test_iter * 1e6
bandwidth = (
hidden_size
* intermediate_size
* 3
* num_experts_per_tok
* (1 / 8 * 256 * (1 - (31 / 32) ** qlen))
* bytes_per_elem
* test_iter
/ total_time
/ 1e9
) # GB/s
flops = (
hidden_size * intermediate_size * qlen * 3 * num_experts_per_tok * 2 * test_iter / total_time / 1e12
) # TFLOPS
print("Quant mode: ", quant_mode)
print("Time(s): ", total_time)
print("Iteration: ", test_iter)
print("Time(us) per iteration: ", time_per_iter_us)
print("Bandwidth: ", bandwidth, "GB/s")
print("Flops: ", flops, "TFLOPS")
# Record results
result = {
"quant_mode": quant_mode,
"total_time_seconds": total_time,
"iterations": test_iter,
"time_per_iteration_us": time_per_iter_us,
"bandwidth_GBs": bandwidth,
"flops_TFLOPS": flops,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"test_parameters": {
"expert_num": expert_num,
"hidden_size": hidden_size,
"intermediate_size": intermediate_size,
"max_len": max_len,
"num_experts_per_tok": num_experts_per_tok,
"layer_num": layer_num,
"qlen": qlen,
"warm_up_iter": warm_up_iter,
"test_iter": test_iter,
"CPUInfer_parameter": CPUINFER_PARAM,
},
}
result.update(get_git_commit())
result.update(get_system_info())
record_results(result, json_path)
print(f"Results saved to {json_path}")
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"Dependencies not available: {import_error}")
print("Skipping AMX MOE INT4 benchmark tests")
return
try:
print("Running AMX MOE INT4 benchmark test...")
test_moe_amx_int4_1_benchmark()
print("AMX MOE INT4 benchmark test passed")
print("\nAll tests passed!")
except Exception as e:
print(f"\nTest failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,329 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT4 1K Group benchmark tests for KT-Kernel.
Benchmarks performance (bandwidth and FLOPS) of AMX-accelerated INT4 MOE operations
with 1K group quantization (AMXInt4_1KGroup_MOE).
"""
import os
import sys
import time
import json
import subprocess
import platform
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 300 seconds
register_cpu_ci(est_time=300, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
from tqdm import tqdm
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from bench_moe_amx_k.py)
expert_num = 16
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 8
layer_num = 2
qlen = 1024
warm_up_iter = 1000
test_iter = 2000
k_group_size = 128
# Worker configuration
worker_config_dict = {
"subpool_count": 2,
"subpool_numa_map": [0, 1],
"subpool_thread_count": [30, 30],
}
CPUINFER_PARAM = 60
def get_git_commit():
"""Get current git commit information."""
result = {}
try:
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip()
commit_msg = subprocess.check_output(["git", "log", "-1", "--pretty=%B"]).decode("utf-8").strip()
result["commit"] = commit
result["commit_message"] = commit_msg
dirty_output = subprocess.check_output(["git", "status", "--porcelain"]).decode("utf-8").strip()
if dirty_output:
result["dirty"] = True
result["dirty_files"] = dirty_output.splitlines()
else:
result["dirty"] = False
except Exception as e:
result["commit"] = None
result["commit_message"] = None
result["dirty"] = None
result["error"] = str(e)
return result
def get_system_info():
"""Get system information including CPU model, memory, cores, and sockets."""
info = {}
uname = platform.uname()
info["system_name"] = uname.system
info["node_name"] = uname.node
# Get CPU model (Linux only)
cpu_model = None
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line:
cpu_model = line.split(":", 1)[1].strip()
break
except Exception as e:
cpu_model = f"Error: {e}"
info["cpu_model"] = cpu_model
# Get memory size in GB (Linux only)
mem_total_gb = None
if os.path.exists("/proc/meminfo"):
try:
with open("/proc/meminfo", "r") as f:
for line in f:
if "MemTotal" in line:
mem_kb = float(line.split(":", 1)[1].split()[0])
mem_total_gb = round(mem_kb / (1024 * 1024), 2)
break
except Exception as e:
mem_total_gb = f"Error: {e}"
info["memory_size_GB"] = mem_total_gb
# Get CPU core count
info["cpu_core_count"] = os.cpu_count()
# Get socket count
sockets = set()
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "physical id" in line:
sockets.add(line.split(":", 1)[1].strip())
except Exception as e:
sockets = set()
info["cpu_socket_count"] = len(sockets) if len(sockets) > 0 else 1
return info
def record_results(result, filename):
"""Append results to JSONL file."""
with open(filename, "a") as f:
f.write(json.dumps(result) + "\n")
@pytest.mark.cpu
def test_moe_amx_int4_1k_benchmark():
"""Benchmark AMX INT4 1K Group MOE performance."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
quant_mode = "int4_1k"
bytes_per_elem = 0.5
# Setup output file
script_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(script_dir, "bench_moe_amx_int4_1k.jsonl")
with torch.inference_mode():
# Initialize CPUInfer with worker config
worker_config = kt_kernel_ext.WorkerPoolConfig()
worker_config.subpool_count = worker_config_dict["subpool_count"]
worker_config.subpool_numa_map = worker_config_dict["subpool_numa_map"]
worker_config.subpool_thread_count = worker_config_dict["subpool_thread_count"]
CPUInfer = kt_kernel_ext.CPUInfer(worker_config)
# Physical to logical map for weight loading
physical_to_logical_map = torch.tensor(data=range(expert_num), device="cpu", dtype=torch.int64).contiguous()
# Initialize MOE layers
moes = []
for layer_index in range(layer_num):
gate_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.pool = CPUInfer.backend_
# Configure quantization for INT4 1K Group
config.quant_config.bits = 4
config.quant_config.group_size = k_group_size
config.quant_config.zero_point = True
config.gate_scale = 0
moe = kt_kernel_ext.moe.AMXInt4_1KGroup_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
moes.append(moe)
# Generate test data
gen_iter = 3000
expert_ids = (
torch.rand(gen_iter * qlen, expert_num, device="cpu")
.argsort(dim=-1)[:, :num_experts_per_tok]
.reshape(gen_iter, qlen * num_experts_per_tok)
.to("cpu")
.contiguous()
)
weights = (
torch.rand((gen_iter, qlen, num_experts_per_tok), dtype=torch.float32, device="cpu").to("cpu").contiguous()
)
input_tensor = (
torch.randn((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
output_tensor = (
torch.empty((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
bsz_tensor = torch.tensor([qlen], device="cpu")
# Warm-up iterations
print(f"Running warm-up for {warm_up_iter} iterations...")
for i in tqdm(range(warm_up_iter), desc="Warm-up"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
# Test iterations
print(f"Running test for {test_iter} iterations...")
start = time.perf_counter()
for i in tqdm(range(test_iter), desc="Testing"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
end = time.perf_counter()
total_time = end - start
# Calculate performance metrics
time_per_iter_us = total_time / test_iter * 1e6
bandwidth = (
hidden_size
* intermediate_size
* 3
* num_experts_per_tok
* (1 / 8 * 256 * (1 - (31 / 32) ** qlen))
* bytes_per_elem
* test_iter
/ total_time
/ 1e9
) # GB/s
flops = (
hidden_size * intermediate_size * qlen * 3 * num_experts_per_tok * 2 * test_iter / total_time / 1e12
) # TFLOPS
print("Quant mode: ", quant_mode)
print("Time(s): ", total_time)
print("Iteration: ", test_iter)
print("Time(us) per iteration: ", time_per_iter_us)
print("Bandwidth: ", bandwidth, "GB/s")
print("Flops: ", flops, "TFLOPS")
# Record results
result = {
"quant_mode": quant_mode,
"total_time_seconds": total_time,
"iterations": test_iter,
"time_per_iteration_us": time_per_iter_us,
"bandwidth_GBs": bandwidth,
"flops_TFLOPS": flops,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"test_parameters": {
"expert_num": expert_num,
"hidden_size": hidden_size,
"intermediate_size": intermediate_size,
"max_len": max_len,
"num_experts_per_tok": num_experts_per_tok,
"layer_num": layer_num,
"qlen": qlen,
"warm_up_iter": warm_up_iter,
"test_iter": test_iter,
"CPUInfer_parameter": CPUINFER_PARAM,
"k_group_size": k_group_size,
},
}
result.update(get_git_commit())
result.update(get_system_info())
record_results(result, json_path)
print(f"Results saved to {json_path}")
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"Dependencies not available: {import_error}")
print("Skipping AMX MOE INT4 1K Group benchmark tests")
return
try:
print("Running AMX MOE INT4 1K Group benchmark test...")
test_moe_amx_int4_1k_benchmark()
print("AMX MOE INT4 1K Group benchmark test passed")
print("\nAll tests passed!")
except Exception as e:
print(f"\nTest failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,317 @@
#!/usr/bin/env python
# coding=utf-8
"""AMX MOE INT8 benchmark tests for KT-Kernel.
Benchmarks performance (bandwidth and FLOPS) of AMX-accelerated INT8 MOE operations.
"""
import os
import sys
import time
import json
import subprocess
import platform
import pytest
# Add parent directory to path for CI registration
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
# Register this test for CPU CI with estimated runtime of 300 seconds
register_cpu_ci(est_time=300, suite="default")
# Check if dependencies are available
try:
import torch
import kt_kernel # Import kt_kernel first to register kt_kernel_ext
kt_kernel_ext = kt_kernel.kt_kernel_ext # Access the extension module
from tqdm import tqdm
HAS_DEPS = True
except ImportError as e:
HAS_DEPS = False
import_error = str(e)
# Test parameters (from original bench_moe_amx.py)
expert_num = 128
hidden_size = 7168
intermediate_size = 2048
max_len = 25600
num_experts_per_tok = 0
layer_num = 2
qlen = 1
warm_up_iter = 1000
test_iter = 2000
# Worker configuration
worker_config_dict = {
"subpool_count": 2,
"subpool_numa_map": [0, 1],
"subpool_thread_count": [30, 30],
}
CPUINFER_PARAM = 60
def get_git_commit():
"""Get current git commit information."""
result = {}
try:
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8").strip()
commit_msg = subprocess.check_output(["git", "log", "-1", "--pretty=%B"]).decode("utf-8").strip()
result["commit"] = commit
result["commit_message"] = commit_msg
dirty_output = subprocess.check_output(["git", "status", "--porcelain"]).decode("utf-8").strip()
if dirty_output:
result["dirty"] = True
result["dirty_files"] = dirty_output.splitlines()
else:
result["dirty"] = False
except Exception as e:
result["commit"] = None
result["commit_message"] = None
result["dirty"] = None
result["error"] = str(e)
return result
def get_system_info():
"""Get system information including CPU model, memory, cores, and sockets."""
info = {}
uname = platform.uname()
info["system_name"] = uname.system
info["node_name"] = uname.node
# Get CPU model (Linux only)
cpu_model = None
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "model name" in line:
cpu_model = line.split(":", 1)[1].strip()
break
except Exception as e:
cpu_model = f"Error: {e}"
info["cpu_model"] = cpu_model
# Get memory size in GB (Linux only)
mem_total_gb = None
if os.path.exists("/proc/meminfo"):
try:
with open("/proc/meminfo", "r") as f:
for line in f:
if "MemTotal" in line:
mem_kb = float(line.split(":", 1)[1].split()[0])
mem_total_gb = round(mem_kb / (1024 * 1024), 2)
break
except Exception as e:
mem_total_gb = f"Error: {e}"
info["memory_size_GB"] = mem_total_gb
# Get CPU core count
info["cpu_core_count"] = os.cpu_count()
# Get socket count
sockets = set()
if os.path.exists("/proc/cpuinfo"):
try:
with open("/proc/cpuinfo", "r") as f:
for line in f:
if "physical id" in line:
sockets.add(line.split(":", 1)[1].strip())
except Exception as e:
sockets = set()
info["cpu_socket_count"] = len(sockets) if len(sockets) > 0 else 1
return info
def record_results(result, filename):
"""Append results to JSONL file."""
with open(filename, "a") as f:
f.write(json.dumps(result) + "\n")
@pytest.mark.cpu
def test_moe_amx_int8_benchmark():
"""Benchmark AMX INT8 MOE performance."""
if not HAS_DEPS:
pytest.skip(f"Dependencies not available: {import_error}")
quant_mode = "int8"
bytes_per_elem = 1.0
# Setup output file
script_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(script_dir, "bench_moe_amx_int8.jsonl")
with torch.inference_mode():
# Initialize CPUInfer with worker config
worker_config = kt_kernel_ext.WorkerPoolConfig()
worker_config.subpool_count = worker_config_dict["subpool_count"]
worker_config.subpool_numa_map = worker_config_dict["subpool_numa_map"]
worker_config.subpool_thread_count = worker_config_dict["subpool_thread_count"]
CPUInfer = kt_kernel_ext.CPUInfer(worker_config)
# Initialize MOE layers
moes = []
for layer_index in range(layer_num):
gate_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
up_proj = (
torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
down_proj = (
torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32, device="cuda")
.to("cpu")
.contiguous()
)
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.pool = CPUInfer.backend_
moe = kt_kernel_ext.moe.AMXInt8_MOE(config)
CPUInfer.submit(moe.load_weights_task())
CPUInfer.sync()
moes.append(moe)
# Generate test data
gen_iter = 3000
expert_ids = (
torch.rand(gen_iter * qlen, expert_num, device="cpu")
.argsort(dim=-1)[:, :num_experts_per_tok]
.reshape(gen_iter, qlen * num_experts_per_tok)
.to("cpu")
.contiguous()
)
weights = (
torch.rand((gen_iter, qlen, num_experts_per_tok), dtype=torch.float32, device="cpu").to("cpu").contiguous()
)
input_tensor = (
torch.randn((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
output_tensor = (
torch.empty((layer_num, qlen, hidden_size), dtype=torch.bfloat16, device="cuda").to("cpu").contiguous()
)
bsz_tensor = torch.tensor([qlen], device="cpu")
# Warm-up iterations
print(f"Running warm-up for {warm_up_iter} iterations...")
for i in tqdm(range(warm_up_iter), desc="Warm-up"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
# Test iterations
print(f"Running test for {test_iter} iterations...")
start = time.perf_counter()
for i in tqdm(range(test_iter), desc="Testing"):
CPUInfer.submit(
moes[i % layer_num].forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids[i % gen_iter].data_ptr(),
weights[i % gen_iter].data_ptr(),
input_tensor[i % layer_num].data_ptr(),
output_tensor[i % layer_num].data_ptr(),
False,
)
)
CPUInfer.sync()
end = time.perf_counter()
total_time = end - start
# Calculate performance metrics
time_per_iter_us = total_time / test_iter * 1e6
bandwidth = (
hidden_size
* intermediate_size
* 3
* num_experts_per_tok
* (1 / 8 * 256 * (1 - (31 / 32) ** qlen))
* bytes_per_elem
* test_iter
/ total_time
/ 1e9
) # GB/s
flops = (
hidden_size * intermediate_size * qlen * 3 * num_experts_per_tok * 2 * test_iter / total_time / 1e12
) # TFLOPS
print("Quant mode: ", quant_mode)
print("Time(s): ", total_time)
print("Iteration: ", test_iter)
print("Time(us) per iteration: ", time_per_iter_us)
print("Bandwidth: ", bandwidth, "GB/s")
print("Flops: ", flops, "TFLOPS")
# Record results
result = {
"quant_mode": quant_mode,
"total_time_seconds": total_time,
"iterations": test_iter,
"time_per_iteration_us": time_per_iter_us,
"bandwidth_GBs": bandwidth,
"flops_TFLOPS": flops,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
"test_parameters": {
"expert_num": expert_num,
"hidden_size": hidden_size,
"intermediate_size": intermediate_size,
"max_len": max_len,
"num_experts_per_tok": num_experts_per_tok,
"layer_num": layer_num,
"qlen": qlen,
"warm_up_iter": warm_up_iter,
"test_iter": test_iter,
"CPUInfer_parameter": CPUINFER_PARAM,
},
}
result.update(get_git_commit())
result.update(get_system_info())
record_results(result, json_path)
print(f"Results saved to {json_path}")
def run_all_tests():
"""Run all tests in this file (for standalone execution)."""
if not HAS_DEPS:
print(f"⚠ Dependencies not available: {import_error}")
print("Skipping AMX MOE INT8 benchmark tests")
return
try:
print("Running AMX MOE INT8 benchmark test...")
test_moe_amx_int8_benchmark()
print("✓ AMX MOE INT8 benchmark test passed")
print("\n✓ All tests passed!")
except Exception as e:
print(f"\n✗ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
run_all_tests()
@@ -0,0 +1,165 @@
#!/usr/bin/env python
# coding=utf-8
"""AVX2 BF16 MoE accuracy tests for KT-Kernel.
Tests accuracy of AVX2 BF16 MOE operations against torch reference.
"""
import os
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
import pytest
import torch
from kt_kernel import kt_kernel_ext
from ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=120, suite="default")
# Small test parameters for fast validation
expert_num = 8
hidden_size = 256
intermediate_size = 512
num_experts_per_tok = 2
max_len = 128
validation_iter = 3
CPUINFER_PARAM = 60
def act_fn(x):
"""SiLU activation."""
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
"""PyTorch reference MLP."""
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
return torch.mm(intermediate, down_proj.t())
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
"""PyTorch reference MoE."""
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
expert_out = mlp_torch(tokens_for_this_expert, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(expert_out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
t_output = (
new_x.view(*expert_ids.shape, -1)
.type(weights.dtype)
.mul_(weights.unsqueeze(dim=-1))
.sum(dim=1)
.type(new_x.dtype)
)
return t_output
@pytest.mark.cpu
@pytest.mark.parametrize("qlen,label", [(1, "Decode"), (16, "Prefill")])
def test_avx2_bf16_accuracy(qlen, label):
"""Test AVX2 BF16 MoE accuracy."""
physical_to_logical_map = torch.tensor(range(expert_num), device="cpu", dtype=torch.int64).contiguous()
CPUInfer = kt_kernel_ext.CPUInfer(CPUINFER_PARAM)
with torch.inference_mode():
# Generate BF16 weights
gate_proj = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16).contiguous()
up_proj = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16).contiguous()
down_proj = (torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32) / 10.0).to(torch.bfloat16).contiguous()
# Create MOE config
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_proj.data_ptr()
config.up_proj = up_proj.data_ptr()
config.down_proj = down_proj.data_ptr()
config.gate_scale = 0
config.up_scale = 0
config.down_scale = 0
config.pool = CPUInfer.backend_
# Create AVX2 BF16 MOE
moe = kt_kernel_ext.moe.AVX2BF16_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
print(f"\n--- {label} (qlen={qlen}) ---")
for i in range(validation_iter):
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = (torch.randn((qlen, hidden_size), dtype=torch.float32) / 100.0).to(torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
bsz_tensor = torch.tensor([qlen], dtype=torch.int32)
# Run AVX2 BF16 MOE
CPUInfer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
CPUInfer.sync()
# Run torch reference (in float32 for accuracy)
t_output = moe_torch(
input_data.float(), expert_ids, weights,
gate_proj.float(), up_proj.float(), down_proj.float()
).to(torch.bfloat16)
# Calculate relative difference
diff = torch.mean(torch.abs(output.float() - t_output.float())) / (torch.mean(torch.abs(t_output.float())) + 1e-8)
print(f" Iteration {i}: diff = {diff:.6f}")
# BF16 should be very accurate (< 0.01)
assert diff < 0.02, f"AVX2 BF16 accuracy test failed: diff={diff:.6f} >= 0.02"
print(f" PASSED")
if __name__ == "__main__":
print("=" * 60)
print("AVX2 BF16 MoE Accuracy Test")
print("=" * 60)
try:
# Test decode path (qlen=1)
test_avx2_bf16_accuracy(qlen=1, label="Decode")
# Test prefill path (qlen=16)
test_avx2_bf16_accuracy(qlen=16, label="Prefill")
print("\n" + "=" * 60)
print("ALL TESTS PASSED")
print("=" * 60)
except Exception as e:
print(f"\nTEST FAILED: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
@@ -0,0 +1,258 @@
#!/usr/bin/env python
# coding=utf-8
"""AVX2 FP8 MoE accuracy tests for KT-Kernel."""
import os
import sys
import math
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
import pytest
import torch
from kt_kernel import kt_kernel_ext
from ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=120, suite="default")
expert_num = 8
hidden_size = 256
intermediate_size = 512
num_experts_per_tok = 2
max_len = 128
group_size = 128
validation_iter = 3
CPUINFER_PARAM = 60
def fp8_e4m3_quantize(tensor_bf16):
"""Quantize BF16 tensor to FP8 E4M3 with block-wise scales (128x128)."""
n, k = tensor_bf16.shape
tensor_fp32 = tensor_bf16.float()
n_blocks_n = (n + group_size - 1) // group_size
n_blocks_k = (k + group_size - 1) // group_size
fp8_data = torch.zeros(n, k, dtype=torch.uint8)
scales = torch.zeros(n_blocks_n, n_blocks_k, dtype=torch.float32)
# FP8 E4M3 max value: 2^8 * (1 + 7/8) = 448
fp8_max = 448.0
for bn in range(n_blocks_n):
for bk in range(n_blocks_k):
n_start = bn * group_size
n_end = min(n_start + group_size, n)
k_start = bk * group_size
k_end = min(k_start + group_size, k)
block = tensor_fp32[n_start:n_end, k_start:k_end]
amax = block.abs().max().item()
if amax == 0:
scale = 1.0
else:
scale = amax / fp8_max
scales[bn, bk] = scale
# Quantize
for i in range(n_end - n_start):
for j in range(k_end - k_start):
val = block[i, j].item() / scale
fp8_data[n_start + i, k_start + j] = float_to_fp8_e4m3(val)
return fp8_data, scales
def float_to_fp8_e4m3(val):
"""Convert float to FP8 E4M3."""
if math.isnan(val):
return 0x7F
sign = 1 if val < 0 else 0
val = abs(val)
if val == 0:
return sign << 7
# Clamp to max
if val >= 448.0:
return (sign << 7) | 0x7E # max finite
# Find exponent
exp = int(math.floor(math.log2(val))) + 7
if exp <= 0:
# Subnormal
man = int(round(val * (2**6) * 8))
man = min(man, 7)
return (sign << 7) | man
if exp >= 15:
return (sign << 7) | 0x7E # clamp to max
# Normal
man = int(round((val / (2**(exp-7)) - 1.0) * 8))
man = min(man, 7)
return (sign << 7) | (exp << 3) | man
def fp8_e4m3_to_float(byte_val):
"""Convert FP8 E4M3 byte to float."""
sign = (byte_val >> 7) & 1
exp = (byte_val >> 3) & 0xF
man = byte_val & 0x7
if exp == 0 and man == 0:
return 0.0
if exp == 0:
val = (2**-6) * (man / 8.0)
elif exp == 15 and man == 7:
# Match the AVX2 LUT: E4M3 has finite exp=15 values up to 0x7e,
# and the NaN sentinel is treated as zero to avoid propagation.
return 0.0
else:
val = (2**(exp-7)) * (1.0 + man / 8.0)
return -val if sign else val
def fp8_dequantize(fp8_data, scales):
"""Dequantize FP8 + scales back to float32."""
n, k = fp8_data.shape
result = torch.zeros(n, k, dtype=torch.float32)
n_blocks_n = scales.shape[0]
n_blocks_k = scales.shape[1]
for i in range(n):
for j in range(k):
bn = i // group_size
bk = j // group_size
scale = scales[bn, bk].item()
fp8_val = fp8_e4m3_to_float(fp8_data[i, j].item())
result[i, j] = fp8_val * scale
return result
def act_fn(x):
return x / (1.0 + torch.exp(-x))
def mlp_torch(input, gate_proj, up_proj, down_proj):
gate_buf = torch.mm(input, gate_proj.t())
up_buf = torch.mm(input, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
return torch.mm(intermediate, down_proj.t())
def moe_torch(input, expert_ids, weights, gate_proj, up_proj, down_proj):
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens = sorted_tokens[start_idx:end_idx]
out = mlp_torch(tokens, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if outputs else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
return (new_x.view(*expert_ids.shape, -1).float().mul_(weights.unsqueeze(-1)).sum(1)).to(new_x.dtype)
@pytest.mark.cpu
@pytest.mark.parametrize("qlen,label", [(1, "Decode"), (16, "Prefill")])
def test_avx2_fp8_accuracy(qlen, label):
physical_to_logical_map = torch.tensor(range(expert_num), dtype=torch.int64).contiguous()
CPUInfer = kt_kernel_ext.CPUInfer(CPUINFER_PARAM)
with torch.inference_mode():
# Generate BF16 weights, quantize to FP8
gate_bf16 = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
up_bf16 = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
down_bf16 = (torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
# Quantize each expert
gate_fp8_list, gate_scale_list = [], []
up_fp8_list, up_scale_list = [], []
down_fp8_list, down_scale_list = [], []
for e in range(expert_num):
gf, gs = fp8_e4m3_quantize(gate_bf16[e])
gate_fp8_list.append(gf)
gate_scale_list.append(gs)
uf, us = fp8_e4m3_quantize(up_bf16[e])
up_fp8_list.append(uf)
up_scale_list.append(us)
df, ds = fp8_e4m3_quantize(down_bf16[e])
down_fp8_list.append(df)
down_scale_list.append(ds)
# Stack into contiguous tensors
gate_fp8 = torch.stack(gate_fp8_list).contiguous()
gate_scales = torch.stack(gate_scale_list).contiguous()
up_fp8 = torch.stack(up_fp8_list).contiguous()
up_scales = torch.stack(up_scale_list).contiguous()
down_fp8 = torch.stack(down_fp8_list).contiguous()
down_scales = torch.stack(down_scale_list).contiguous()
# Dequantize for reference computation
gate_deq = torch.stack([fp8_dequantize(gate_fp8_list[e], gate_scale_list[e]) for e in range(expert_num)])
up_deq = torch.stack([fp8_dequantize(up_fp8_list[e], up_scale_list[e]) for e in range(expert_num)])
down_deq = torch.stack([fp8_dequantize(down_fp8_list[e], down_scale_list[e]) for e in range(expert_num)])
# Create MOE config
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_fp8.data_ptr()
config.up_proj = up_fp8.data_ptr()
config.down_proj = down_fp8.data_ptr()
config.gate_scale = gate_scales.data_ptr()
config.up_scale = up_scales.data_ptr()
config.down_scale = down_scales.data_ptr()
config.quant_config.bits = 8
config.quant_config.group_size = group_size
config.quant_config.zero_point = False
config.pool = CPUInfer.backend_
moe = kt_kernel_ext.moe.AVX2FP8_MOE(config)
CPUInfer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
CPUInfer.sync()
print("\n--- %s (qlen=%d) ---" % (label, qlen))
for i in range(validation_iter):
expert_ids = torch.stack([torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = (torch.randn((qlen, hidden_size), dtype=torch.float32) / 100.0).to(torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
bsz_tensor = torch.tensor([qlen], dtype=torch.int32)
CPUInfer.submit(moe.forward_task(
bsz_tensor.data_ptr(), num_experts_per_tok,
expert_ids.data_ptr(), weights.data_ptr(),
input_data.data_ptr(), output.data_ptr(), False,
))
CPUInfer.sync()
# Reference: use dequantized FP32 weights
t_output = moe_torch(input_data.float(), expert_ids, weights, gate_deq, up_deq, down_deq).to(torch.bfloat16)
diff = torch.mean(torch.abs(output.float() - t_output.float())) / (torch.mean(torch.abs(t_output.float())) + 1e-8)
print(" Iteration %d: diff = %.6f" % (i, diff.item()))
assert diff < 0.1, "FP8 accuracy test failed: diff=%.6f >= 0.1" % diff.item()
print(" PASSED")
if __name__ == "__main__":
print("=" * 60)
print("AVX2 FP8 MoE Accuracy Test")
print("=" * 60)
try:
test_avx2_fp8_accuracy(qlen=1, label="Decode")
test_avx2_fp8_accuracy(qlen=16, label="Prefill")
print("\n" + "=" * 60)
print("ALL TESTS PASSED")
print("=" * 60)
except Exception as e:
print("\nTEST FAILED: %s" % e)
import traceback
traceback.print_exc()
sys.exit(1)
@@ -0,0 +1,317 @@
#!/usr/bin/env python
# coding=utf-8
"""GPTQ INT4 MoE accuracy tests for KT-Kernel x86 backends."""
import importlib.util
import os
import sys
import types
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python"))
register_cpu_ci(est_time=120, suite="default")
import pytest
import torch
import kt_kernel_ext
KT_KERNEL_ROOT = Path(__file__).resolve().parents[2]
expert_num = 8
hidden_size = 256
intermediate_size = 512
num_experts_per_tok = 2
max_len = 128
group_size = 128
validation_iter = 3
CPUINFER_PARAM = 16
def load_amx_utils():
pkg_root = KT_KERNEL_ROOT / "python"
utils_root = pkg_root / "utils"
if "kt_kernel" not in sys.modules:
kt_kernel_pkg = types.ModuleType("kt_kernel")
kt_kernel_pkg.__path__ = [str(pkg_root)]
kt_kernel_pkg.kt_kernel_ext = kt_kernel_ext
sys.modules["kt_kernel"] = kt_kernel_pkg
if "kt_kernel_ext" not in sys.modules:
sys.modules["kt_kernel_ext"] = kt_kernel_ext
if "kt_kernel.utils" not in sys.modules:
utils_pkg = types.ModuleType("kt_kernel.utils")
utils_pkg.__path__ = [str(utils_root)]
sys.modules["kt_kernel.utils"] = utils_pkg
module_specs = [
("kt_kernel.experts_base", pkg_root / "experts_base.py"),
("kt_kernel.utils.loader", utils_root / "loader.py"),
("kt_kernel.utils.amx", utils_root / "amx.py"),
]
for module_name, module_path in module_specs:
if module_name in sys.modules:
continue
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
assert spec.loader is not None
spec.loader.exec_module(module)
return sys.modules["kt_kernel.utils.amx"]
def gptq_sym_int4_quantize(weight_bf16):
"""Quantize [N, K] BF16 weight to GPTQ symmetric int4 layout."""
n, k = weight_bf16.shape
assert k % 8 == 0
assert k % group_size == 0
weight_fp32 = weight_bf16.float()
qweight = torch.zeros((k // 8, n), dtype=torch.int32)
scales = torch.zeros((k // group_size, n), dtype=torch.float32)
for ni in range(n):
for g in range(k // group_size):
k_start = g * group_size
k_end = k_start + group_size
block = weight_fp32[ni, k_start:k_end]
amax = block.abs().max().item()
scale = amax / 7.0 if amax > 0 else 1.0
scales[g, ni] = scale
for kk in range(k_start, k_end, 8):
packed = 0
for nib in range(8):
q = int(round(weight_fp32[ni, kk + nib].item() / scale)) + 8
q = max(0, min(15, q))
packed |= q << (nib * 4)
if packed >= 2**31:
packed -= 2**32
qweight[kk // 8, ni] = packed
return qweight, scales
def gptq_sym_int4_dequantize(qweight, scales, out_features, in_features):
"""Dequantize GPTQ qweight/scales back to fp32 [N, K]."""
result = torch.zeros((out_features, in_features), dtype=torch.float32)
for ni in range(out_features):
for g in range(in_features // group_size):
scale = scales[g, ni].item()
k_start = g * group_size
k_end = k_start + group_size
for kk in range(k_start, k_end, 8):
packed = int(qweight[kk // 8, ni].item())
for nib in range(8):
result[ni, kk + nib] = (((packed >> (nib * 4)) & 0xF) - 8) * scale
return result
def act_fn(x):
return x / (1.0 + torch.exp(-x))
def mlp_torch(input_data, gate_proj, up_proj, down_proj):
gate_buf = torch.mm(input_data, gate_proj.t())
up_buf = torch.mm(input_data, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
return torch.mm(intermediate, down_proj.t())
def moe_torch(input_data, expert_ids, weights, gate_proj, up_proj, down_proj):
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input_data[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens = sorted_tokens[start_idx:end_idx]
out = mlp_torch(tokens, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if outputs else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
return (new_x.view(*expert_ids.shape, -1).float().mul_(weights.unsqueeze(-1)).sum(1)).to(new_x.dtype)
def available_backends():
backends = []
if hasattr(kt_kernel_ext.moe, "AVX2GPTQInt4_MOE"):
backends.append(("AVX2GPTQInt4_MOE", kt_kernel_ext.moe.AVX2GPTQInt4_MOE, 0.12))
if hasattr(kt_kernel_ext.moe, "AVXVNNI256GPTQInt4_MOE"):
has_avx_vnni = False
try:
with open("/proc/cpuinfo", "r") as f:
has_avx_vnni = any(("avx_vnni" in line or "avxvnni" in line) for line in f if line.startswith("flags"))
except OSError:
has_avx_vnni = False
if has_avx_vnni:
backends.append(("AVXVNNI256GPTQInt4_MOE", kt_kernel_ext.moe.AVXVNNI256GPTQInt4_MOE, 0.20))
return backends
def run_backend_accuracy_test(backend_name, backend_cls, threshold, qlen):
physical_to_logical_map = torch.tensor(range(expert_num), dtype=torch.int64).contiguous()
cpu_infer = kt_kernel_ext.CPUInfer(CPUINFER_PARAM)
with torch.inference_mode():
gate_bf16 = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(
torch.bfloat16
)
up_bf16 = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(
torch.bfloat16
)
down_bf16 = (torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32) / 10.0).to(
torch.bfloat16
)
gate_qw_list, gate_scale_list = [], []
up_qw_list, up_scale_list = [], []
down_qw_list, down_scale_list = [], []
for e in range(expert_num):
qw, sc = gptq_sym_int4_quantize(gate_bf16[e])
gate_qw_list.append(qw)
gate_scale_list.append(sc)
qw, sc = gptq_sym_int4_quantize(up_bf16[e])
up_qw_list.append(qw)
up_scale_list.append(sc)
qw, sc = gptq_sym_int4_quantize(down_bf16[e])
down_qw_list.append(qw)
down_scale_list.append(sc)
gate_qw = torch.stack(gate_qw_list).contiguous()
gate_scales = torch.stack(gate_scale_list).contiguous()
up_qw = torch.stack(up_qw_list).contiguous()
up_scales = torch.stack(up_scale_list).contiguous()
down_qw = torch.stack(down_qw_list).contiguous()
down_scales = torch.stack(down_scale_list).contiguous()
gate_deq = torch.stack(
[
gptq_sym_int4_dequantize(gate_qw_list[e], gate_scale_list[e], intermediate_size, hidden_size)
for e in range(expert_num)
]
)
up_deq = torch.stack(
[
gptq_sym_int4_dequantize(up_qw_list[e], up_scale_list[e], intermediate_size, hidden_size)
for e in range(expert_num)
]
)
down_deq = torch.stack(
[
gptq_sym_int4_dequantize(down_qw_list[e], down_scale_list[e], hidden_size, intermediate_size)
for e in range(expert_num)
]
)
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_qw.data_ptr()
config.up_proj = up_qw.data_ptr()
config.down_proj = down_qw.data_ptr()
config.gate_scale = gate_scales.data_ptr()
config.up_scale = up_scales.data_ptr()
config.down_scale = down_scales.data_ptr()
config.quant_config.bits = 4
config.quant_config.group_size = group_size
config.quant_config.zero_point = False
config.pool = cpu_infer.backend_
moe = backend_cls(config)
cpu_infer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
cpu_infer.sync()
print(f"\n--- {backend_name} (qlen={qlen}) ---")
for i in range(validation_iter):
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = (torch.randn((qlen, hidden_size), dtype=torch.float32) / 100.0).to(torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
bsz_tensor = torch.tensor([qlen], dtype=torch.int32)
cpu_infer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
cpu_infer.sync()
ref_output = moe_torch(input_data.float(), expert_ids, weights, gate_deq, up_deq, down_deq).to(
torch.bfloat16
)
diff = torch.mean(torch.abs(output.float() - ref_output.float())) / (
torch.mean(torch.abs(ref_output.float())) + 1e-8
)
print(f" Iteration {i}: diff = {diff.item():.6f}")
assert diff < threshold, f"{backend_name} accuracy test failed: diff={diff.item():.6f} >= {threshold}"
def test_gptq_int4_accuracy():
backends = available_backends()
if not backends:
print("Skipping GPTQ INT4 accuracy tests: no x86 GPTQ backend available")
return
for backend_name, backend_cls, threshold in backends:
run_backend_accuracy_test(backend_name, backend_cls, threshold, qlen=1)
run_backend_accuracy_test(backend_name, backend_cls, threshold, qlen=16)
def test_gptq_int4_backend_selection_falls_back_to_avx2_for_large_group_size(monkeypatch):
amx_utils = load_amx_utils()
fake_avx2_backend = object()
fake_avxvnni_backend = object()
monkeypatch.setattr(amx_utils, "AVX2GPTQInt4_MOE", fake_avx2_backend)
monkeypatch.setattr(amx_utils, "AVXVNNI256GPTQInt4_MOE", fake_avxvnni_backend)
monkeypatch.setattr(amx_utils, "_HAS_AVX2_GPTQ_INT4_SUPPORT", True)
monkeypatch.setattr(amx_utils, "_HAS_AVXVNNI256_GPTQ_INT4_SUPPORT", True)
monkeypatch.setattr(amx_utils, "_HOST_HAS_AVX_VNNI", True)
monkeypatch.delenv("KT_GPTQ_INT4_BACKEND", raising=False)
assert amx_utils._select_gptq_int4_backend(512) is fake_avx2_backend
assert amx_utils._select_gptq_int4_backend(128) is fake_avxvnni_backend
def test_gptq_int4_backend_selection_rejects_forced_avxvnni_with_large_group_size(monkeypatch):
amx_utils = load_amx_utils()
monkeypatch.setattr(amx_utils, "_HAS_AVXVNNI256_GPTQ_INT4_SUPPORT", True)
monkeypatch.setattr(amx_utils, "_HOST_HAS_AVX_VNNI", True)
monkeypatch.setenv("KT_GPTQ_INT4_BACKEND", "avxvnni")
with pytest.raises(RuntimeError, match="group_size=512 is unsupported"):
amx_utils._select_gptq_int4_backend(512)
if __name__ == "__main__":
print("=" * 60)
print("GPTQ INT4 MoE Accuracy Test")
print("=" * 60)
test_gptq_int4_accuracy()
print("PASSED")
@@ -0,0 +1,395 @@
#!/usr/bin/env python
# coding=utf-8
"""RAWINT4 MoE accuracy tests for KT-Kernel x86 backends."""
import importlib.util
import os
import sys
import types
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ci.ci_register import register_cpu_ci
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python"))
register_cpu_ci(est_time=120, suite="default")
import pytest
import torch
import kt_kernel_ext
KT_KERNEL_ROOT = Path(__file__).resolve().parents[2]
expert_num = 8
hidden_size = 256
intermediate_size = 512
num_experts_per_tok = 2
max_len = 128
group_size = 128
validation_iter = 3
CPUINFER_PARAM = 16
def load_amx_utils():
pkg_root = KT_KERNEL_ROOT / "python"
utils_root = pkg_root / "utils"
if "kt_kernel" not in sys.modules:
kt_kernel_pkg = types.ModuleType("kt_kernel")
kt_kernel_pkg.__path__ = [str(pkg_root)]
kt_kernel_pkg.kt_kernel_ext = kt_kernel_ext
sys.modules["kt_kernel"] = kt_kernel_pkg
if "kt_kernel_ext" not in sys.modules:
sys.modules["kt_kernel_ext"] = kt_kernel_ext
if "kt_kernel.utils" not in sys.modules:
utils_pkg = types.ModuleType("kt_kernel.utils")
utils_pkg.__path__ = [str(utils_root)]
sys.modules["kt_kernel.utils"] = utils_pkg
module_specs = [
("kt_kernel.experts_base", pkg_root / "experts_base.py"),
("kt_kernel.utils.loader", utils_root / "loader.py"),
("kt_kernel.utils.amx", utils_root / "amx.py"),
]
for module_name, module_path in module_specs:
if module_name in sys.modules:
continue
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
assert spec.loader is not None
spec.loader.exec_module(module)
return sys.modules["kt_kernel.utils.amx"]
def rawint4_quantize(weight_bf16):
"""Quantize [N, K] BF16 weight to RAWINT4 layout."""
n, k = weight_bf16.shape
assert k % 2 == 0
assert k % group_size == 0
weight_fp32 = weight_bf16.float()
qweight = torch.zeros((n, k // 2), dtype=torch.uint8)
scales = torch.zeros((n, k // group_size), dtype=torch.bfloat16)
for ni in range(n):
for g in range(k // group_size):
k_start = g * group_size
k_end = k_start + group_size
block = weight_fp32[ni, k_start:k_end]
amax = block.abs().max().item()
scale = amax / 7.0 if amax > 0 else 1.0
scales[ni, g] = scale
for kk in range(k_start, k_end, 2):
q0 = int(round(weight_fp32[ni, kk].item() / scale)) + 8
q1 = int(round(weight_fp32[ni, kk + 1].item() / scale)) + 8
q0 = max(0, min(15, q0))
q1 = max(0, min(15, q1))
qweight[ni, kk // 2] = (q1 << 4) | q0
return qweight, scales
def rawint4_dequantize(qweight, scales, out_features, in_features):
"""Dequantize RAWINT4 qweight/scales back to fp32 [N, K]."""
result = torch.zeros((out_features, in_features), dtype=torch.float32)
for ni in range(out_features):
for g in range(in_features // group_size):
scale = scales[ni, g].float().item()
k_start = g * group_size
k_end = k_start + group_size
for kk in range(k_start, k_end, 2):
packed = int(qweight[ni, kk // 2].item())
result[ni, kk] = ((packed & 0x0F) - 8) * scale
result[ni, kk + 1] = (((packed >> 4) & 0x0F) - 8) * scale
return result
def pack_rawint4_uint8_as_int32(qweight):
"""Pack byte RAWINT4 layout into compressed-tensors int32 storage."""
assert qweight.dtype == torch.uint8
assert qweight.shape[1] % 4 == 0
return qweight.contiguous().view(torch.int32).contiguous()
def act_fn(x):
return x / (1.0 + torch.exp(-x))
def mlp_torch(input_data, gate_proj, up_proj, down_proj):
gate_buf = torch.mm(input_data, gate_proj.t())
up_buf = torch.mm(input_data, up_proj.t())
intermediate = act_fn(gate_buf) * up_buf
return torch.mm(intermediate, down_proj.t())
def moe_torch(input_data, expert_ids, weights, gate_proj, up_proj, down_proj):
cnts = expert_ids.new_zeros((expert_ids.shape[0], expert_num))
cnts.scatter_(1, expert_ids, 1)
tokens_per_expert = cnts.sum(dim=0)
idxs = expert_ids.view(-1).argsort()
sorted_tokens = input_data[idxs // expert_ids.shape[1]]
outputs = []
start_idx = 0
for i, num_tokens in enumerate(tokens_per_expert):
end_idx = start_idx + num_tokens
if num_tokens == 0:
continue
tokens = sorted_tokens[start_idx:end_idx]
out = mlp_torch(tokens, gate_proj[i], up_proj[i], down_proj[i])
outputs.append(out)
start_idx = end_idx
outs = torch.cat(outputs, dim=0) if outputs else sorted_tokens.new_empty(0)
new_x = torch.empty_like(outs)
new_x[idxs] = outs
return (new_x.view(*expert_ids.shape, -1).float().mul_(weights.unsqueeze(-1)).sum(1)).to(new_x.dtype)
def available_backends():
backends = []
if hasattr(kt_kernel_ext.moe, "AVX2RawInt4_MOE"):
backends.append(("AVX2RawInt4_MOE", kt_kernel_ext.moe.AVX2RawInt4_MOE, 0.12))
if hasattr(kt_kernel_ext.moe, "AVXVNNI256RawInt4_MOE"):
has_avx_vnni = False
try:
with open("/proc/cpuinfo", "r") as f:
has_avx_vnni = any(("avx_vnni" in line or "avxvnni" in line) for line in f if line.startswith("flags"))
except OSError:
has_avx_vnni = False
if has_avx_vnni:
backends.append(("AVXVNNI256RawInt4_MOE", kt_kernel_ext.moe.AVXVNNI256RawInt4_MOE, 0.20))
return backends
def run_backend_accuracy_test(backend_name, backend_cls, threshold, qlen):
physical_to_logical_map = torch.tensor(range(expert_num), dtype=torch.int64).contiguous()
cpu_infer = kt_kernel_ext.CPUInfer(CPUINFER_PARAM)
with torch.inference_mode():
gate_bf16 = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(
torch.bfloat16
)
up_bf16 = (torch.randn((expert_num, intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(
torch.bfloat16
)
down_bf16 = (torch.randn((expert_num, hidden_size, intermediate_size), dtype=torch.float32) / 10.0).to(
torch.bfloat16
)
gate_qw_list, gate_scale_list = [], []
up_qw_list, up_scale_list = [], []
down_qw_list, down_scale_list = [], []
for e in range(expert_num):
qw, sc = rawint4_quantize(gate_bf16[e])
gate_qw_list.append(qw)
gate_scale_list.append(sc)
qw, sc = rawint4_quantize(up_bf16[e])
up_qw_list.append(qw)
up_scale_list.append(sc)
qw, sc = rawint4_quantize(down_bf16[e])
down_qw_list.append(qw)
down_scale_list.append(sc)
gate_qw = torch.stack(gate_qw_list).contiguous()
gate_scales = torch.stack(gate_scale_list).contiguous()
up_qw = torch.stack(up_qw_list).contiguous()
up_scales = torch.stack(up_scale_list).contiguous()
down_qw = torch.stack(down_qw_list).contiguous()
down_scales = torch.stack(down_scale_list).contiguous()
gate_deq = torch.stack(
[
rawint4_dequantize(gate_qw_list[e], gate_scale_list[e], intermediate_size, hidden_size)
for e in range(expert_num)
]
)
up_deq = torch.stack(
[
rawint4_dequantize(up_qw_list[e], up_scale_list[e], intermediate_size, hidden_size)
for e in range(expert_num)
]
)
down_deq = torch.stack(
[
rawint4_dequantize(down_qw_list[e], down_scale_list[e], hidden_size, intermediate_size)
for e in range(expert_num)
]
)
config = kt_kernel_ext.moe.MOEConfig(expert_num, num_experts_per_tok, hidden_size, intermediate_size, 0)
config.max_len = max_len
config.gate_proj = gate_qw.data_ptr()
config.up_proj = up_qw.data_ptr()
config.down_proj = down_qw.data_ptr()
config.gate_scale = gate_scales.data_ptr()
config.up_scale = up_scales.data_ptr()
config.down_scale = down_scales.data_ptr()
config.quant_config.bits = 4
config.quant_config.group_size = group_size
config.quant_config.zero_point = False
config.pool = cpu_infer.backend_
moe = backend_cls(config)
cpu_infer.submit(moe.load_weights_task(physical_to_logical_map.data_ptr()))
cpu_infer.sync()
print(f"\n--- {backend_name} (qlen={qlen}) ---")
for i in range(validation_iter):
expert_ids = torch.stack(
[torch.randperm(expert_num)[:num_experts_per_tok] for _ in range(qlen)]
).contiguous()
weights = torch.rand((qlen, num_experts_per_tok), dtype=torch.float32).contiguous()
input_data = (torch.randn((qlen, hidden_size), dtype=torch.float32) / 100.0).to(torch.bfloat16).contiguous()
output = torch.empty((qlen, hidden_size), dtype=torch.bfloat16).contiguous()
bsz_tensor = torch.tensor([qlen], dtype=torch.int32)
cpu_infer.submit(
moe.forward_task(
bsz_tensor.data_ptr(),
num_experts_per_tok,
expert_ids.data_ptr(),
weights.data_ptr(),
input_data.data_ptr(),
output.data_ptr(),
False,
)
)
cpu_infer.sync()
ref_output = moe_torch(input_data.float(), expert_ids, weights, gate_deq, up_deq, down_deq).to(
torch.bfloat16
)
diff = torch.mean(torch.abs(output.float() - ref_output.float())) / (
torch.mean(torch.abs(ref_output.float())) + 1e-8
)
print(f" Iteration {i}: diff = {diff.item():.6f}")
assert diff < threshold, f"{backend_name} accuracy test failed: diff={diff.item():.6f} >= {threshold}"
def test_rawint4_accuracy():
backends = available_backends()
if not backends:
print("Skipping RAWINT4 accuracy tests: no x86 RAWINT4 backend available")
return
for backend_name, backend_cls, threshold in backends:
run_backend_accuracy_test(backend_name, backend_cls, threshold, qlen=1)
run_backend_accuracy_test(backend_name, backend_cls, threshold, qlen=16)
def test_compressed_loader_normalizes_int32_pack_quantized_weights():
load_amx_utils()
loader_mod = sys.modules["kt_kernel.utils.loader"]
weight_bf16 = (torch.randn((intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
qweight, scales = rawint4_quantize(weight_bf16)
packed_int32 = pack_rawint4_uint8_as_int32(qweight)
weight_shape = torch.tensor([intermediate_size, hidden_size], dtype=torch.int32)
normalized = loader_mod.CompressedSafeTensorLoader._normalize_rawint4_weight(
packed_int32, scales, weight_shape, "test.weight_packed"
)
assert normalized.dtype == torch.uint8
assert normalized.shape == qweight.shape
assert torch.equal(normalized, qweight)
def test_compressed_loader_accepts_uint8_rawint4_weights():
load_amx_utils()
loader_mod = sys.modules["kt_kernel.utils.loader"]
weight_bf16 = (torch.randn((intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
qweight, scales = rawint4_quantize(weight_bf16)
weight_shape = torch.tensor([intermediate_size, hidden_size], dtype=torch.int32)
normalized = loader_mod.CompressedSafeTensorLoader._normalize_rawint4_weight(
qweight, scales, weight_shape, "test.weight_packed"
)
assert normalized.dtype == torch.uint8
assert normalized.shape == qweight.shape
assert torch.equal(normalized, qweight)
def test_compressed_loader_ignores_invalid_weight_shape_metadata():
load_amx_utils()
loader_mod = sys.modules["kt_kernel.utils.loader"]
weight_bf16 = (torch.randn((intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
qweight, scales = rawint4_quantize(weight_bf16)
packed_int32 = pack_rawint4_uint8_as_int32(qweight)
invalid_shape = torch.tensor([-1752796263, -1707567530], dtype=torch.int32)
normalized = loader_mod.CompressedSafeTensorLoader._normalize_rawint4_weight(
packed_int32, scales, invalid_shape, "test.weight_packed"
)
assert normalized.dtype == torch.uint8
assert normalized.shape == qweight.shape
assert torch.equal(normalized, qweight)
def test_compressed_loader_ignores_odd_weight_shape_metadata():
load_amx_utils()
loader_mod = sys.modules["kt_kernel.utils.loader"]
weight_bf16 = (torch.randn((intermediate_size, hidden_size), dtype=torch.float32) / 10.0).to(torch.bfloat16)
qweight, scales = rawint4_quantize(weight_bf16)
packed_int32 = pack_rawint4_uint8_as_int32(qweight)
invalid_shape = torch.tensor([241597647, 1216029047], dtype=torch.int32)
normalized = loader_mod.CompressedSafeTensorLoader._normalize_rawint4_weight(
packed_int32, scales, invalid_shape, "test.weight_packed"
)
assert normalized.dtype == torch.uint8
assert normalized.shape == qweight.shape
assert torch.equal(normalized, qweight)
def test_rawint4_backend_selection_falls_back_to_avx2_for_large_group_size(monkeypatch):
amx_utils = load_amx_utils()
fake_amx_backend = object()
fake_avx2_backend = object()
fake_avxvnni_backend = object()
monkeypatch.setattr(amx_utils, "AMXInt4_KGroup_MOE", fake_amx_backend)
monkeypatch.setattr(amx_utils, "AVX2RawInt4_MOE", fake_avx2_backend)
monkeypatch.setattr(amx_utils, "AVXVNNI256RawInt4_MOE", fake_avxvnni_backend)
monkeypatch.setattr(amx_utils, "_HAS_RAWINT4_SUPPORT", False)
monkeypatch.setattr(amx_utils, "_HAS_AVX2_RAWINT4_SUPPORT", True)
monkeypatch.setattr(amx_utils, "_HAS_AVXVNNI256_RAW_INT4_SUPPORT", True)
monkeypatch.setattr(amx_utils, "_HOST_HAS_AVX_VNNI", True)
monkeypatch.delenv("KT_RAWINT4_BACKEND", raising=False)
assert amx_utils._select_rawint4_backend(512) is fake_avx2_backend
assert amx_utils._select_rawint4_backend(128) is fake_avxvnni_backend
def test_rawint4_backend_selection_rejects_forced_avxvnni_with_large_group_size(monkeypatch):
amx_utils = load_amx_utils()
monkeypatch.setattr(amx_utils, "_HAS_AVXVNNI256_RAW_INT4_SUPPORT", True)
monkeypatch.setattr(amx_utils, "_HOST_HAS_AVX_VNNI", True)
monkeypatch.setenv("KT_RAWINT4_BACKEND", "avxvnni")
with pytest.raises(RuntimeError, match="group_size=512 is unsupported"):
amx_utils._select_rawint4_backend(512)
if __name__ == "__main__":
print("=" * 60)
print("RAWINT4 MoE Accuracy Test")
print("=" * 60)
test_rawint4_accuracy()
print("PASSED")
@@ -0,0 +1,45 @@
import importlib.util
import socket
from pathlib import Path
import unittest
from unittest.mock import MagicMock, patch
from ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=0.1, suite="default")
PORT_CHECKER_PATH = Path(__file__).resolve().parents[2] / "python" / "cli" / "utils" / "port_checker.py"
SPEC = importlib.util.spec_from_file_location("port_checker", PORT_CHECKER_PATH)
assert SPEC is not None and SPEC.loader is not None
port_checker = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(port_checker)
class TestPortChecker(unittest.TestCase):
def test_bound_port_is_not_available_before_listen(self):
holder = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
holder.bind(("127.0.0.1", 0))
port = holder.getsockname()[1]
self.assertFalse(port_checker.is_port_available("127.0.0.1", port))
self.assertEqual(port_checker.find_available_port("127.0.0.1", port, max_attempts=1), (False, port))
finally:
holder.close()
def test_non_windows_bind_check_uses_reuseaddr(self):
sock = MagicMock()
sock.__enter__.return_value = sock
with patch.object(port_checker.sys, "platform", "linux"):
with patch.object(port_checker.socket, "socket", return_value=sock):
self.assertTrue(port_checker.is_port_available("127.0.0.1", 12345))
sock.setsockopt.assert_called_once_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind.assert_called_once_with(("127.0.0.1", 12345))
if __name__ == "__main__":
unittest.main()