From 488dfaf7056eaaf46b2d3ad5d8b644ca067f85ca Mon Sep 17 00:00:00 2001 From: Jagggged Date: Tue, 30 Jun 2026 16:00:51 +0900 Subject: [PATCH 01/11] [Frontend] Implement abs operator and add pointwise test --- PyTorchSimFrontend/mlir/mlir_ops.py | 5 ++++- tests/test_pointwise.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/test_pointwise.py diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index 58e8b73b..deaf57f3 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -309,7 +309,10 @@ def binary_elementwise_common(operand1, operand2): @staticmethod def abs(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype + opcode = "math.absf" if dtype.startswith("f") else "math.absi" + return format_mlir_op(f'{opcode} %{operand}', shape, **kwargs), [tile_size, dtype] @staticmethod def exp(operand, *args, **kwargs): diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py new file mode 100644 index 00000000..7517c38f --- /dev/null +++ b/tests/test_pointwise.py @@ -0,0 +1,32 @@ +import torch + +def test_result(name, out, cpu_out, rtol=1e-4, atol=1e-4): + if torch.allclose(out.cpu(), cpu_out, rtol=rtol, atol=atol): + message = f"|{name} Test Passed|" + print("-" * len(message)) + print(message) + print("-" * len(message)) + else: + message = f"|{name} Test Failed|" + print("-" * len(message)) + print(message) + print("-" * len(message)) + print("custom out: ", out.cpu()) + print("cpu out: ", cpu_out) + exit(1) + +def test_abs(device, size=(128, 128)): + def abs_fn(a): + return torch.abs(a) + + x = (torch.randn(size) * 10).to(device=device) + opt_fn = torch.compile(dynamic=False)(abs_fn) + res = opt_fn(x) + out = abs_fn(x.cpu()) + test_result("Abs", res, out) + +if __name__ == "__main__": + device = torch.device("npu:0") + + test_abs(device, size=(128, 128)) + test_abs(device, size=(1, 1)) \ No newline at end of file From e56d05b29d63793897757b54daa29740f005f59f Mon Sep 17 00:00:00 2001 From: Jagggged Date: Tue, 30 Jun 2026 19:30:34 +0900 Subject: [PATCH 02/11] [Frontend] Implement sign, fmod, isinf, isnan and update pointwise test --- PyTorchSimFrontend/mlir/mlir_ops.py | 75 ++++++++++++++++++--- tests/test_pointwise.py | 101 +++++++++++++++++++++++++++- 2 files changed, 167 insertions(+), 9 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index deaf57f3..01167185 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -467,25 +467,45 @@ def erf(operand, *args, **kwargs): @staticmethod def cosh(operand, *args, **kwargs): raise NotImplementedError + # tile_size, dtype = V.kernel.var_info[operand] + # + # # Check scalar + # if tile_size == 1: + # operand = ops.broadcast(operand, 4) + # val = ops.cosh(operand) + # result = ops.extractelement(val, 0) + # return result, V.kernel.var_info[result] + # + # shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype + # return format_mlir_op(f'math.cosh %{operand}', shape, **kwargs), [tile_size, dtype] @staticmethod def sinh(operand, *args, **kwargs): raise NotImplementedError + # tile_size, dtype = V.kernel.var_info[operand] + # + # # Check scalar + # if tile_size == 1: + # operand = ops.broadcast(operand, 4) + # val = ops.sinh(operand) + # result = ops.extractelement(val, 0) + # return result, V.kernel.var_info[result] + # + # shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype + # return format_mlir_op(f'math.sinh %{operand}', shape, **kwargs), [tile_size, dtype] @staticmethod def tanh(operand, *args, **kwargs): op_type = V.kernel.var_info[operand] + tile_size = op_type[0] + dtype = op_type[1] # Check scalar - op_type = V.kernel.var_info[operand] if op_type[0] == 1: operand = ops.broadcast(operand, 4) val = ops.tanh(operand) result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - op_type = V.kernel.var_info[operand] - tile_size = op_type[0] - dtype = op_type[1] # Type check & auto cast if dtype.startswith("f"): @@ -692,15 +712,44 @@ def sigmoid(operand, *args, **kwargs): @staticmethod def fmod(operand1, operand2, *args, **kwargs): - raise NotImplementedError + tile_size, ret_type, operand1, operand2 = ExtensionOverrides.binary_elementwise_common(operand1, operand2) + shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type + + opcode = "arith.remf" if ret_type.startswith("f") else "arith.remsi" + + op_str = f'{opcode} %{operand1}, %{operand2}' + return format_mlir_op(op_str, shape, **kwargs), [tile_size, ret_type] @staticmethod def isinf(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + if dtype.startswith("f"): + abs_val = ops.abs(operand) + inf_val = ops.constant("inf", dtype) + res = ops.eq(abs_val, inf_val) + return res, V.kernel.var_info[res] + else: + const_false = ops.constant(False, "i1") + if tile_size > 1: + const_false = ops.broadcast(const_false, tile_size) + return const_false, V.kernel.var_info[const_false] @staticmethod def isnan(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + if dtype.startswith("f"): + # Unordered comparison (uno) to detect NaN (uno returns true if either operand is NaN) + shape = f"vector<{tile_size}xi1>" if tile_size > 1 else "i1" + op_str = f"arith.cmpf uno, %{operand}, %{operand}" + return format_mlir_op(op_str, shape, **kwargs), [tile_size, "i1"] + else: + # Integers cannot be NaN + const_false = ops.constant(False, "i1") + if tile_size > 1: + const_false = ops.broadcast(const_false, tile_size) + return const_false, V.kernel.var_info[const_false] @staticmethod def round(operand, *args, **kwargs): @@ -726,7 +775,17 @@ def floor(operand, *args, **kwargs): @staticmethod def sign(operand, *args, **kwargs): - raise NotImplementedError + dtype = V.kernel.var_info[operand][1] + + const_zero = ops.constant(0, dtype) + const_one = ops.constant(1, dtype) + const_neg_one = ops.constant(-1, dtype) + + is_pos = ops.gt(operand, const_zero) + is_neg = ops.lt(operand, const_zero) + + res = ops.where(is_pos, const_one, ops.where(is_neg, const_neg_one, const_zero)) + return res, V.kernel.var_info[res] @staticmethod def trunc(operand, *args, **kwargs): diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index 7517c38f..19d424a3 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -23,10 +23,109 @@ def abs_fn(a): opt_fn = torch.compile(dynamic=False)(abs_fn) res = opt_fn(x) out = abs_fn(x.cpu()) + + # Print input and output values (print first 5 if size is large) + input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] Abs Input: {input_val}") + print(f"[{size}] Abs Output: {output_val}") + test_result("Abs", res, out) +def test_sign(device, size=(128, 128)): + def sign_fn(a): + return torch.sign(a) + + x = torch.randn(size).to(device=device) + x[x.abs() < 0.3] = 0.0 + + opt_fn = torch.compile(dynamic=False)(sign_fn) + res = opt_fn(x) + out = sign_fn(x.cpu()) + + # Print input and output values (print first 10 if size is large) + input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] Sign Input: {input_val}") + print(f"[{size}] Sign Output: {output_val}") + + test_result("Sign", res, out) + +def test_isnan(device, size=(128, 128)): + def isnan_fn(a): + return torch.isnan(a) + + # Generate random floats on CPU and inject NaNs, then move to NPU + x_cpu = torch.randn(size) + x_cpu[x_cpu.abs() < 0.3] = float('nan') + x = x_cpu.to(device=device) + + opt_fn = torch.compile(dynamic=False)(isnan_fn) + res = opt_fn(x) + out = isnan_fn(x.cpu()) + + input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] IsNaN Input: {input_val}") + print(f"[{size}] IsNaN Output: {output_val}") + + test_result("IsNaN", res, out) + +def test_isinf(device, size=(128, 128)): + def isinf_fn(a): + return torch.isinf(a) + + # Generate random floats and inject positive/negative infinities + x_cpu = torch.randn(size) + x_cpu[x_cpu > 1.0] = float('inf') + x_cpu[x_cpu < -1.0] = float('-inf') + x = x_cpu.to(device=device) + + opt_fn = torch.compile(dynamic=False)(isinf_fn) + res = opt_fn(x) + out = isinf_fn(x.cpu()) + + input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] IsInf Input: {input_val}") + print(f"[{size}] IsInf Output: {output_val}") + + test_result("IsInf", res, out) + +def test_fmod(device, size=(128, 128)): + def fmod_fn(a, b): + return torch.fmod(a, b) + + x = (torch.randn(size) * 10).to(device=device) + y = (torch.randn(size) * 3 + 1).to(device=device) # Avoid dividing by zero + + opt_fn = torch.compile(dynamic=False)(fmod_fn) + res = opt_fn(x, y) + out = fmod_fn(x.cpu(), y.cpu()) + + input_val_x = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + input_val_y = y.flatten()[:10].tolist() if size != (1, 1) else y.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] Fmod Input X: {input_val_x}") + print(f"[{size}] Fmod Input Y: {input_val_y}") + print(f"[{size}] Fmod Output: {output_val}") + + test_result("Fmod", res, out) + if __name__ == "__main__": device = torch.device("npu:0") test_abs(device, size=(128, 128)) - test_abs(device, size=(1, 1)) \ No newline at end of file + test_abs(device, size=(1, 1)) + + test_sign(device, size=(128, 128)) + test_sign(device, size=(1, 1)) + + test_isnan(device, size=(128, 128)) + test_isnan(device, size=(1, 1)) + + test_isinf(device, size=(128, 128)) + test_isinf(device, size=(1, 1)) + + test_fmod(device, size=(128, 128)) + test_fmod(device, size=(1, 1)) \ No newline at end of file From 0921a5d44635fd8378fada62ed1a6a5f60bf5f6d Mon Sep 17 00:00:00 2001 From: Jagggged Date: Tue, 30 Jun 2026 19:58:41 +0900 Subject: [PATCH 03/11] [Frontend] Implement bitwise shift operators and update pointwise test --- PyTorchSimFrontend/mlir/mlir_ops.py | 18 ++++++++--- tests/test_pointwise.py | 48 ++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index 01167185..3526d7d4 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -683,11 +683,21 @@ def bitwise_xor(operand1, operand2, *args, **kwargs): @staticmethod def bitwise_left_shift(operand1, operand2, *args, **kwargs): - raise NotImplementedError + tile_size, ret_type, operand1, operand2 = ExtensionOverrides.binary_elementwise_common(operand1, operand2) + if ret_type.startswith("f"): + raise ValueError("Bitwise left shift not supported for floats") + shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type + op_str = f'arith.shli %{operand1}, %{operand2}' + return format_mlir_op(op_str, shape, **kwargs), [tile_size, ret_type] @staticmethod def bitwise_right_shift(operand1, operand2, *args, **kwargs): - raise NotImplementedError + tile_size, ret_type, operand1, operand2 = ExtensionOverrides.binary_elementwise_common(operand1, operand2) + if ret_type.startswith("f"): + raise ValueError("Bitwise right shift not supported for floats") + shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type + op_str = f'arith.shrsi %{operand1}, %{operand2}' + return format_mlir_op(op_str, shape, **kwargs), [tile_size, ret_type] @staticmethod def rsqrt(operand, *args, **kwargs): @@ -995,11 +1005,11 @@ def xor(operand1, operand2, *args, **kwargs): @staticmethod def lshift(operand1, operand2, *args, **kwargs): - raise NotImplementedError + return ops.bitwise_left_shift(operand1, operand2) @staticmethod def rshift(operand1, operand2, *args, **kwargs): - raise NotImplementedError + return ops.bitwise_right_shift(operand1, operand2) @staticmethod def truncdiv(operand1, operand2, *args, **kwargs): diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index 19d424a3..2077096c 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -112,6 +112,46 @@ def fmod_fn(a, b): test_result("Fmod", res, out) +def test_lshift(device, size=(128, 128)): + def lshift_fn(a, b): + return torch.bitwise_left_shift(a, b) + + x = torch.randint(1, 100, size, dtype=torch.int32).to(device=device) + y = torch.randint(1, 5, size, dtype=torch.int32).to(device=device) + + opt_fn = torch.compile(dynamic=False)(lshift_fn) + res = opt_fn(x, y) + out = lshift_fn(x.cpu(), y.cpu()) + + input_val_x = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + input_val_y = y.flatten()[:10].tolist() if size != (1, 1) else y.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] LShift Input X: {input_val_x}") + print(f"[{size}] LShift Input Y: {input_val_y}") + print(f"[{size}] LShift Output: {output_val}") + + test_result("LShift", res, out) + +def test_rshift(device, size=(128, 128)): + def rshift_fn(a, b): + return torch.bitwise_right_shift(a, b) + + x = torch.randint(10, 1000, size, dtype=torch.int32).to(device=device) + y = torch.randint(1, 5, size, dtype=torch.int32).to(device=device) + + opt_fn = torch.compile(dynamic=False)(rshift_fn) + res = opt_fn(x, y) + out = rshift_fn(x.cpu(), y.cpu()) + + input_val_x = x.flatten()[:10].tolist() if size != (1, 1) else x.item() + input_val_y = y.flatten()[:10].tolist() if size != (1, 1) else y.item() + output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() + print(f"[{size}] RShift Input X: {input_val_x}") + print(f"[{size}] RShift Input Y: {input_val_y}") + print(f"[{size}] RShift Output: {output_val}") + + test_result("RShift", res, out) + if __name__ == "__main__": device = torch.device("npu:0") @@ -128,4 +168,10 @@ def fmod_fn(a, b): test_isinf(device, size=(1, 1)) test_fmod(device, size=(128, 128)) - test_fmod(device, size=(1, 1)) \ No newline at end of file + test_fmod(device, size=(1, 1)) + + test_lshift(device, size=(128, 128)) + test_lshift(device, size=(1, 1)) + + test_rshift(device, size=(128, 128)) + test_rshift(device, size=(1, 1)) \ No newline at end of file From 502258e1a961bffde566b99796645a8ffa1e6a2f Mon Sep 17 00:00:00 2001 From: Jagggged Date: Wed, 1 Jul 2026 13:17:56 +0900 Subject: [PATCH 04/11] [Frontend] Implement copysign, erfc, hypot operators and update pointwise test --- PyTorchSimFrontend/mlir/mlir_ops.py | 30 ++- tests/test_pointwise.py | 315 ++++++++++++++++------------ 2 files changed, 207 insertions(+), 138 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index 3526d7d4..e0ffc4e3 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -543,11 +543,27 @@ def atanh(operand, *args, **kwargs): @staticmethod def copysign(operand1, operand2, *args, **kwargs): - raise NotImplementedError + tile_size, ret_type, operand1, operand2 = ExtensionOverrides.binary_elementwise_common(operand1, operand2) + if not ret_type.startswith("f"): + raise ValueError("copysign is only supported for floats") + shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type + op_str = f'math.copysign %{operand1}, %{operand2}' + return format_mlir_op(op_str, shape, **kwargs), [tile_size, ret_type] @staticmethod def erfc(operand, *args, **kwargs): - raise NotImplementedError + # Check scalar + op_type = V.kernel.var_info[operand] + if op_type[0] == 1: + operand = ops.broadcast(operand, 4) + val = ops.erfc(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + tile_size = op_type[0] + dtype = op_type[1] + shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype + return format_mlir_op(f'math.erfc %{operand}', shape, **kwargs), [tile_size, dtype] @staticmethod def erfinv(operand, *args, **kwargs): @@ -559,7 +575,15 @@ def frexp(operand, *args, **kwargs): @staticmethod def hypot(operand1, operand2, *args, **kwargs): - raise NotImplementedError + tile_size, ret_type, operand1, operand2 = ExtensionOverrides.binary_elementwise_common(operand1, operand2) + if not ret_type.startswith("f"): + raise ValueError("hypot is only supported for floats") + + x_sq = ops.square(operand1) + y_sq = ops.square(operand2) + sum_sq = ops.add(x_sq, y_sq) + res = ops.sqrt(sum_sq) + return res, V.kernel.var_info[res] @staticmethod def log10(operand, *args, **kwargs): diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index 2077096c..382f04a7 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -15,163 +15,208 @@ def test_result(name, out, cpu_out, rtol=1e-4, atol=1e-4): print("cpu out: ", cpu_out) exit(1) -def test_abs(device, size=(128, 128)): +def run_test(name, device, fn, inputs, size_desc, rtol=1e-4, atol=1e-4): + """ + Harness function to compile, execute on NPU, compare with CPU, and print details. + inputs: single tensor or tuple/list of tensors (on CPU) + """ + if not isinstance(inputs, (tuple, list)): + inputs = [inputs] + + npu_inputs = [x.to(device=device) for x in inputs] + cpu_inputs = [x.clone() for x in inputs] + + opt_fn = torch.compile(dynamic=False)(fn) + res = opt_fn(*npu_inputs) + out = fn(*cpu_inputs) + + # Print input / output slices (up to 10 elements) + for idx, x in enumerate(inputs): + label = f"X" if len(inputs) == 1 else f"X{idx+1}" + val = x.flatten()[:10].tolist() if x.numel() > 1 else x.item() + print(f"[{size_desc}] {name} Input {label}: {val}") + + out_val = res.flatten()[:10].tolist() if res.numel() > 1 else res.item() + print(f"[{size_desc}] {name} Output: {out_val}") + + test_result(f"{name}_{size_desc}", res, out, rtol=rtol, atol=atol) + +def test_abs(device): def abs_fn(a): return torch.abs(a) - x = (torch.randn(size) * 10).to(device=device) - opt_fn = torch.compile(dynamic=False)(abs_fn) - res = opt_fn(x) - out = abs_fn(x.cpu()) - - # Print input and output values (print first 5 if size is large) - input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] Abs Input: {input_val}") - print(f"[{size}] Abs Output: {output_val}") - - test_result("Abs", res, out) - -def test_sign(device, size=(128, 128)): + # 1. Float Vector (Aligned) + run_test("Abs_Float", device, abs_fn, torch.randn(128, 128) * 10, "128x128") + # 2. Float Scalar + run_test("Abs_Float", device, abs_fn, torch.randn(1, 1) * 10, "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Abs_Float", device, abs_fn, torch.randn(15, 15) * 10, "15x15") + run_test("Abs_Float", device, abs_fn, torch.randn(129, 129) * 10, "129x129") + # 4. Int Vector (Aligned) + run_test("Abs_Int", device, abs_fn, torch.randint(-100, 100, (128, 128), dtype=torch.int32), "128x128_int") + # 5. Non-contiguous (Transposed & Strided Sliced) + run_test("Abs_Float_Strided", device, abs_fn, (torch.randn(128, 128) * 10).t(), "128x128_strided_transposed") + run_test("Abs_Float_Strided", device, abs_fn, (torch.randn(128, 256) * 10)[:, ::2], "128x128_strided_sliced") + +def test_sign(device): def sign_fn(a): return torch.sign(a) - x = torch.randn(size).to(device=device) - x[x.abs() < 0.3] = 0.0 - - opt_fn = torch.compile(dynamic=False)(sign_fn) - res = opt_fn(x) - out = sign_fn(x.cpu()) - - # Print input and output values (print first 10 if size is large) - input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] Sign Input: {input_val}") - print(f"[{size}] Sign Output: {output_val}") - - test_result("Sign", res, out) - -def test_isnan(device, size=(128, 128)): + # 1. Float Vector (Aligned) + x_float = torch.randn(128, 128) + x_float[x_float.abs() < 0.3] = 0.0 + run_test("Sign_Float", device, sign_fn, x_float, "128x128") + # 2. Float Scalar (includes zero and nonzero) + x_scalar = torch.tensor([[0.0]]) + run_test("Sign_Float", device, sign_fn, x_scalar, "1x1_zero") + x_scalar_non_zero = torch.tensor([[-4.5]]) + run_test("Sign_Float", device, sign_fn, x_scalar_non_zero, "1x1_nonzero") + # 3. Float Vector (Tail / Remainder - Small & Large) + x_tail = torch.randn(15, 15) + x_tail[x_tail.abs() < 0.3] = 0.0 + run_test("Sign_Float", device, sign_fn, x_tail, "15x15") + x_tail_large = torch.randn(129, 129) + x_tail_large[x_tail_large.abs() < 0.3] = 0.0 + run_test("Sign_Float", device, sign_fn, x_tail_large, "129x129") + # 4. Int Vector (Aligned) + x_int = torch.randint(-5, 5, (128, 128), dtype=torch.int32) + run_test("Sign_Int", device, sign_fn, x_int, "128x128_int") + +def test_isnan(device): def isnan_fn(a): return torch.isnan(a) - # Generate random floats on CPU and inject NaNs, then move to NPU - x_cpu = torch.randn(size) - x_cpu[x_cpu.abs() < 0.3] = float('nan') - x = x_cpu.to(device=device) - - opt_fn = torch.compile(dynamic=False)(isnan_fn) - res = opt_fn(x) - out = isnan_fn(x.cpu()) - - input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] IsNaN Input: {input_val}") - print(f"[{size}] IsNaN Output: {output_val}") - - test_result("IsNaN", res, out) - -def test_isinf(device, size=(128, 128)): + # 1. Float Vector with NaNs (Aligned) + x = torch.randn(128, 128) + x[x.abs() < 0.3] = float('nan') + run_test("IsNaN", device, isnan_fn, x, "128x128") + # 2. Float Scalar + x_scalar = torch.tensor([[float('nan')]]) + run_test("IsNaN", device, isnan_fn, x_scalar, "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + x_tail = torch.randn(15, 15) + x_tail[x_tail.abs() < 0.3] = float('nan') + run_test("IsNaN", device, isnan_fn, x_tail, "15x15") + x_tail_large = torch.randn(129, 129) + x_tail_large[x_tail_large.abs() < 0.3] = float('nan') + run_test("IsNaN", device, isnan_fn, x_tail_large, "129x129") + +def test_isinf(device): def isinf_fn(a): return torch.isinf(a) - # Generate random floats and inject positive/negative infinities - x_cpu = torch.randn(size) - x_cpu[x_cpu > 1.0] = float('inf') - x_cpu[x_cpu < -1.0] = float('-inf') - x = x_cpu.to(device=device) - - opt_fn = torch.compile(dynamic=False)(isinf_fn) - res = opt_fn(x) - out = isinf_fn(x.cpu()) - - input_val = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] IsInf Input: {input_val}") - print(f"[{size}] IsInf Output: {output_val}") - - test_result("IsInf", res, out) - -def test_fmod(device, size=(128, 128)): + # 1. Float Vector with Infs (Aligned) + x = torch.randn(128, 128) + x[x > 1.0] = float('inf') + x[x < -1.0] = float('-inf') + run_test("IsInf", device, isinf_fn, x, "128x128") + # 2. Float Scalar + x_scalar = torch.tensor([[float('-inf')]]) + run_test("IsInf", device, isinf_fn, x_scalar, "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + x_tail = torch.randn(15, 15) + x_tail[x_tail > 1.0] = float('inf') + x_tail[x_tail < -1.0] = float('-inf') + run_test("IsInf", device, isinf_fn, x_tail, "15x15") + x_tail_large = torch.randn(129, 129) + x_tail_large[x_tail_large > 1.0] = float('inf') + x_tail_large[x_tail_large < -1.0] = float('-inf') + run_test("IsInf", device, isinf_fn, x_tail_large, "129x129") + +def test_fmod(device): def fmod_fn(a, b): return torch.fmod(a, b) - x = (torch.randn(size) * 10).to(device=device) - y = (torch.randn(size) * 3 + 1).to(device=device) # Avoid dividing by zero - - opt_fn = torch.compile(dynamic=False)(fmod_fn) - res = opt_fn(x, y) - out = fmod_fn(x.cpu(), y.cpu()) - - input_val_x = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - input_val_y = y.flatten()[:10].tolist() if size != (1, 1) else y.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] Fmod Input X: {input_val_x}") - print(f"[{size}] Fmod Input Y: {input_val_y}") - print(f"[{size}] Fmod Output: {output_val}") - - test_result("Fmod", res, out) - -def test_lshift(device, size=(128, 128)): + # 1. Float Vector (Aligned) + x = torch.randn(128, 128) * 10 + y = torch.randn(128, 128) * 3 + 1 + run_test("Fmod", device, fmod_fn, (x, y), "128x128") + # 2. Float Scalar + run_test("Fmod", device, fmod_fn, (torch.tensor([[5.5]]), torch.tensor([[2.0]])), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Fmod", device, fmod_fn, (torch.randn(15, 15) * 10, torch.randn(15, 15) * 3 + 1), "15x15") + run_test("Fmod", device, fmod_fn, (torch.randn(129, 129) * 10, torch.randn(129, 129) * 3 + 1), "129x129") + # 4. Broadcasting (128x1 vs 1x128) + run_test("Fmod_Broadcast", device, fmod_fn, (torch.randn(128, 1) * 10, torch.randn(1, 128) * 3 + 1), "broadcast") + +def test_lshift(device): def lshift_fn(a, b): return torch.bitwise_left_shift(a, b) - x = torch.randint(1, 100, size, dtype=torch.int32).to(device=device) - y = torch.randint(1, 5, size, dtype=torch.int32).to(device=device) - - opt_fn = torch.compile(dynamic=False)(lshift_fn) - res = opt_fn(x, y) - out = lshift_fn(x.cpu(), y.cpu()) - - input_val_x = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - input_val_y = y.flatten()[:10].tolist() if size != (1, 1) else y.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] LShift Input X: {input_val_x}") - print(f"[{size}] LShift Input Y: {input_val_y}") - print(f"[{size}] LShift Output: {output_val}") - - test_result("LShift", res, out) - -def test_rshift(device, size=(128, 128)): + # 1. Int Vector (Aligned) + run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (128, 128), dtype=torch.int32), torch.randint(1, 5, (128, 128), dtype=torch.int32)), "128x128") + # 2. Int Scalar + run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (1, 1), dtype=torch.int32), torch.randint(1, 5, (1, 1), dtype=torch.int32)), "1x1") + # 3. Int Vector (Tail / Remainder - Small & Large) + run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (15, 15), dtype=torch.int32), torch.randint(1, 5, (15, 15), dtype=torch.int32)), "15x15") + run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (129, 129), dtype=torch.int32), torch.randint(1, 5, (129, 129), dtype=torch.int32)), "129x129") + # 4. Broadcasting (128x1 vs 1x128) + run_test("LShift_Broadcast", device, lshift_fn, (torch.randint(1, 100, (128, 1), dtype=torch.int32), torch.randint(1, 5, (1, 128), dtype=torch.int32)), "broadcast") + +def test_rshift(device): def rshift_fn(a, b): return torch.bitwise_right_shift(a, b) - x = torch.randint(10, 1000, size, dtype=torch.int32).to(device=device) - y = torch.randint(1, 5, size, dtype=torch.int32).to(device=device) - - opt_fn = torch.compile(dynamic=False)(rshift_fn) - res = opt_fn(x, y) - out = rshift_fn(x.cpu(), y.cpu()) - - input_val_x = x.flatten()[:10].tolist() if size != (1, 1) else x.item() - input_val_y = y.flatten()[:10].tolist() if size != (1, 1) else y.item() - output_val = res.flatten()[:10].tolist() if size != (1, 1) else res.item() - print(f"[{size}] RShift Input X: {input_val_x}") - print(f"[{size}] RShift Input Y: {input_val_y}") - print(f"[{size}] RShift Output: {output_val}") - - test_result("RShift", res, out) + # 1. Int Vector (Aligned) + run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (128, 128), dtype=torch.int32), torch.randint(1, 5, (128, 128), dtype=torch.int32)), "128x128") + # 2. Int Scalar + run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (1, 1), dtype=torch.int32), torch.randint(1, 5, (1, 1), dtype=torch.int32)), "1x1") + # 3. Int Vector (Tail / Remainder - Small & Large) + run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (15, 15), dtype=torch.int32), torch.randint(1, 5, (15, 15), dtype=torch.int32)), "15x15") + run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (129, 129), dtype=torch.int32), torch.randint(1, 5, (129, 129), dtype=torch.int32)), "129x129") + # 4. Broadcasting (128x1 vs 1x128) + run_test("RShift_Broadcast", device, rshift_fn, (torch.randint(10, 1000, (128, 1), dtype=torch.int32), torch.randint(1, 5, (1, 128), dtype=torch.int32)), "broadcast") + +def test_copysign(device): + def copysign_fn(a, b): + return torch.copysign(a, b) + + # 1. Float Vector (Aligned) + run_test("Copysign", device, copysign_fn, (torch.randn(128, 128) * 10, torch.randn(128, 128)), "128x128") + # 2. Float Scalar (test sign of zero case specifically: negative zero) + run_test("Copysign", device, copysign_fn, (torch.tensor([[3.0]]), torch.tensor([[-0.0]])), "1x1_negzero") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Copysign", device, copysign_fn, (torch.randn(15, 15) * 10, torch.randn(15, 15)), "15x15") + run_test("Copysign", device, copysign_fn, (torch.randn(129, 129) * 10, torch.randn(129, 129)), "129x129") + # 4. Broadcasting (128x1 vs 1x128) + run_test("Copysign_Broadcast", device, copysign_fn, (torch.randn(128, 1) * 10, torch.randn(1, 128)), "broadcast") + +def test_erfc(device): + def erfc_fn(a): + return torch.erfc(a) + + # 1. Float Vector (Aligned) + run_test("Erfc", device, erfc_fn, torch.randn(128, 128), "128x128") + # 2. Float Scalar (test large positive float) + run_test("Erfc", device, erfc_fn, torch.tensor([[4.5]]), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Erfc", device, erfc_fn, torch.randn(15, 15), "15x15") + run_test("Erfc", device, erfc_fn, torch.randn(129, 129), "129x129") + +def test_hypot(device): + def hypot_fn(a, b): + return torch.hypot(a, b) + + # 1. Float Vector (Aligned) + run_test("Hypot", device, hypot_fn, (torch.randn(128, 128), torch.randn(128, 128)), "128x128") + # 2. Float Scalar (Result should be exactly 5.0) + run_test("Hypot", device, hypot_fn, (torch.tensor([[3.0]]), torch.tensor([[4.0]])), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Hypot", device, hypot_fn, (torch.randn(15, 15), torch.randn(15, 15)), "15x15") + run_test("Hypot", device, hypot_fn, (torch.randn(129, 129), torch.randn(129, 129)), "129x129") + # 4. Broadcasting (128x1 vs 1x128) + run_test("Hypot_Broadcast", device, hypot_fn, (torch.randn(128, 1), torch.randn(1, 128)), "broadcast") if __name__ == "__main__": device = torch.device("npu:0") - test_abs(device, size=(128, 128)) - test_abs(device, size=(1, 1)) - - test_sign(device, size=(128, 128)) - test_sign(device, size=(1, 1)) - - test_isnan(device, size=(128, 128)) - test_isnan(device, size=(1, 1)) - - test_isinf(device, size=(128, 128)) - test_isinf(device, size=(1, 1)) - - test_fmod(device, size=(128, 128)) - test_fmod(device, size=(1, 1)) - - test_lshift(device, size=(128, 128)) - test_lshift(device, size=(1, 1)) - - test_rshift(device, size=(128, 128)) - test_rshift(device, size=(1, 1)) \ No newline at end of file + test_abs(device) + test_sign(device) + test_isnan(device) + test_isinf(device) + test_fmod(device) + test_lshift(device) + test_rshift(device) + test_copysign(device) + test_erfc(device) + test_hypot(device) \ No newline at end of file From e476033c59c5d36eb4422c3cca52ffd76b7df756 Mon Sep 17 00:00:00 2001 From: Jagggged Date: Tue, 7 Jul 2026 13:55:43 +0900 Subject: [PATCH 05/11] [Frontend] Implement log as VCIX vector instruction --- PyTorchSimFrontend/extension_codecache.py | 2 +- PyTorchSimFrontend/mlir/mlir_ops.py | 6 ++++++ gem5_script/vpu_config.py | 1 + tests/test_pointwise.py | 15 ++++++++++++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/PyTorchSimFrontend/extension_codecache.py b/PyTorchSimFrontend/extension_codecache.py index 6192c47b..969b2536 100644 --- a/PyTorchSimFrontend/extension_codecache.py +++ b/PyTorchSimFrontend/extension_codecache.py @@ -95,7 +95,7 @@ def mlir_gem5_compile_command(filename, sample_filename, tog_file, vectorlane_si -dma-fine-grained='systolic-array-size={vectorlane_size}' \ -global-idx='vlen={vlen}' \ -test-pytorchsim-to-vcix='systolic-array-size={vectorlane_size} vlen={vlen}' \ - -test-tile-operation-graph='vectorlane={vectorlane_size} sample-mode={extension_config.CONFIG_TLS_MODE}' \ + -test-tile-operation-graph='vectorlane={vectorlane_size} tls_mode={extension_config.CONFIG_TLS_MODE}' \ -test-memref-to-gemmini="vectorlane={vectorlane_size} timing=1" \ -convert-linalg-to-loops \ -convert-vector-to-scf='full-unroll' \ diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index e0ffc4e3..1077b9cf 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -611,12 +611,18 @@ def log2(operand, *args, **kwargs): @staticmethod def log(operand, *args, **kwargs): op_type = V.kernel.var_info[operand] + if op_type[0] == 1: + operand = ops.broadcast(operand, 4) + val = ops.log(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] tile_size = op_type[0] dtype = op_type[1] # Type check & auto cast if dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") + dtype = "f32" shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype return format_mlir_op(f'math.log %{operand}', shape, **kwargs), [tile_size, dtype] diff --git a/gem5_script/vpu_config.py b/gem5_script/vpu_config.py index 33d26b5f..329f323c 100644 --- a/gem5_script/vpu_config.py +++ b/gem5_script/vpu_config.py @@ -20,6 +20,7 @@ class SpecialFunctionUnit(MinorFU): "CustomVtanh", "CustomVsin", "CustomVcos", + "CustomVlog", ]) opLat = 10 diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index 382f04a7..e70f964a 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -207,6 +207,18 @@ def hypot_fn(a, b): # 4. Broadcasting (128x1 vs 1x128) run_test("Hypot_Broadcast", device, hypot_fn, (torch.randn(128, 1), torch.randn(1, 128)), "broadcast") +def test_log(device): + def log_fn(a): + return torch.log(a) + + # 1. Float Vector (Aligned) + run_test("Log", device, log_fn, torch.rand(128, 128) + 1e-5, "128x128") # Avoid log(0) + # 2. Float Scalar + run_test("Log", device, log_fn, torch.tensor([[2.71828]]), "1x1") # log(e) = 1 + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Log", device, log_fn, torch.rand(15, 15) + 1e-5, "15x15") + run_test("Log", device, log_fn, torch.rand(129, 129) + 1e-5, "129x129") + if __name__ == "__main__": device = torch.device("npu:0") @@ -219,4 +231,5 @@ def hypot_fn(a, b): test_rshift(device) test_copysign(device) test_erfc(device) - test_hypot(device) \ No newline at end of file + test_hypot(device) + test_log(device) \ No newline at end of file From e9cfdb55ad88334f3f2ac4be2745834dc50f6754 Mon Sep 17 00:00:00 2001 From: Jagggged Date: Tue, 7 Jul 2026 13:58:49 +0900 Subject: [PATCH 06/11] [Frontend] Implement cosh/sinh/acosh/asinh/atanh via exp/log decomposition --- PyTorchSimFrontend/mlir/mlir_ops.py | 193 +++++++++++++++++++++------- tests/test_pointwise.py | 106 +++++++++++++-- 2 files changed, 241 insertions(+), 58 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index 1077b9cf..a15c5a9f 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -145,7 +145,7 @@ def where(condition, operand1, operand2, *args, **kwargs): operand2 = ops.broadcast(operand2, cond_type[0]) tile_size, ret_type = V.kernel.var_info[operand1] shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type - cond_shape = f"vector<{tile_size}xi1>" if tile_size > 1 else "" + cond_shape = f"vector<{tile_size}xi1>" if tile_size > 1 else "i1" op_str = f"arith.select %{condition}, %{operand1}, %{operand2}" shape = f"{cond_shape}, {shape}" @@ -466,33 +466,51 @@ def erf(operand, *args, **kwargs): @staticmethod def cosh(operand, *args, **kwargs): - raise NotImplementedError - # tile_size, dtype = V.kernel.var_info[operand] - # - # # Check scalar - # if tile_size == 1: - # operand = ops.broadcast(operand, 4) - # val = ops.cosh(operand) - # result = ops.extractelement(val, 0) - # return result, V.kernel.var_info[result] - # - # shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype - # return format_mlir_op(f'math.cosh %{operand}', shape, **kwargs), [tile_size, dtype] + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.cosh(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + exp_pos = ops.exp(operand) + exp_neg = ops.exp(ops.neg(operand)) + + sum_exp = ops.add(exp_pos, exp_neg) + half_const = ops.constant(0.5, dtype) + + res = ops.mul(sum_exp, half_const) + return res, V.kernel.var_info[res] @staticmethod def sinh(operand, *args, **kwargs): - raise NotImplementedError - # tile_size, dtype = V.kernel.var_info[operand] - # - # # Check scalar - # if tile_size == 1: - # operand = ops.broadcast(operand, 4) - # val = ops.sinh(operand) - # result = ops.extractelement(val, 0) - # return result, V.kernel.var_info[result] - # - # shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype - # return format_mlir_op(f'math.sinh %{operand}', shape, **kwargs), [tile_size, dtype] + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.sinh(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + exp_pos = ops.exp(operand) + exp_neg = ops.exp(ops.neg(operand)) + + sub_exp = ops.sub(exp_pos, exp_neg) + half_const = ops.constant(0.5, dtype) + + res = ops.mul(sub_exp, half_const) + return res, V.kernel.var_info[res] @staticmethod def tanh(operand, *args, **kwargs): @@ -519,7 +537,26 @@ def acos(operand, *args, **kwargs): @staticmethod def acosh(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.acosh(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + x2 = ops.square(operand) + val = ops.sub(x2, ops.constant(1.0, dtype)) + sqrt_val = ops.sqrt(val) + sum_val = ops.add(operand, sqrt_val) + + res = ops.log(sum_val) + return res, V.kernel.var_info[res] @staticmethod def asin(operand, *args, **kwargs): @@ -527,7 +564,26 @@ def asin(operand, *args, **kwargs): @staticmethod def asinh(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.asinh(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + x2 = ops.square(operand) + val = ops.add(x2, ops.constant(1.0, dtype)) + sqrt_val = ops.sqrt(val) + sum_val = ops.add(operand, sqrt_val) + + res = ops.log(sum_val) + return res, V.kernel.var_info[res] @staticmethod def atan2(operand1, operand2, *args, **kwargs): @@ -539,7 +595,27 @@ def atan(operand, *args, **kwargs): @staticmethod def atanh(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.atanh(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + one_const = ops.constant(1.0, dtype) + val1 = ops.add(one_const, operand) + val2 = ops.sub(one_const, operand) + div_val = ops.truediv(val1, val2) + + half_const = ops.constant(0.5, dtype) + res = ops.mul(ops.log(div_val), half_const) + return res, V.kernel.var_info[res] @staticmethod def copysign(operand1, operand2, *args, **kwargs): @@ -552,8 +628,13 @@ def copysign(operand1, operand2, *args, **kwargs): @staticmethod def erfc(operand, *args, **kwargs): - # Check scalar + """ + There is no direct MLIR operation for erfc, so we can implement it using the relationship: + erfc(x) = 1 - erf(x) + """ op_type = V.kernel.var_info[operand] + + # Check scalar if op_type[0] == 1: operand = ops.broadcast(operand, 4) val = ops.erfc(operand) @@ -562,8 +643,11 @@ def erfc(operand, *args, **kwargs): tile_size = op_type[0] dtype = op_type[1] - shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype - return format_mlir_op(f'math.erfc %{operand}', shape, **kwargs), [tile_size, dtype] + erf_val = ops.erf(operand) + one_const = ops.constant(1.0, dtype) + res = ops.sub(one_const, erf_val) + + return res, V.kernel.var_info[res] @staticmethod def erfinv(operand, *args, **kwargs): @@ -753,12 +837,17 @@ def sigmoid(operand, *args, **kwargs): @staticmethod def fmod(operand1, operand2, *args, **kwargs): tile_size, ret_type, operand1, operand2 = ExtensionOverrides.binary_elementwise_common(operand1, operand2) - shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type - - opcode = "arith.remf" if ret_type.startswith("f") else "arith.remsi" - - op_str = f'{opcode} %{operand1}, %{operand2}' - return format_mlir_op(op_str, shape, **kwargs), [tile_size, ret_type] + + if ret_type.startswith("f"): + div_val = ops.truediv(operand1, operand2) + trunc_val = ops.trunc(div_val) + mul_val = ops.mul(trunc_val, operand2) + res = ops.sub(operand1, mul_val) + return res, V.kernel.var_info[res] + else: + shape = f"vector<{tile_size}x{ret_type}>" if tile_size > 1 else ret_type + op_str = f'arith.remsi %{operand1}, %{operand2}' + return format_mlir_op(op_str, shape, **kwargs), [tile_size, ret_type] @staticmethod def isinf(operand, *args, **kwargs): @@ -778,12 +867,15 @@ def isinf(operand, *args, **kwargs): @staticmethod def isnan(operand, *args, **kwargs): tile_size, dtype = V.kernel.var_info[operand] - + print(f"Checking isnan for operand with dtype: {dtype} and tile_size: {tile_size}") if dtype.startswith("f"): # Unordered comparison (uno) to detect NaN (uno returns true if either operand is NaN) - shape = f"vector<{tile_size}xi1>" if tile_size > 1 else "i1" + operand_shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype op_str = f"arith.cmpf uno, %{operand}, %{operand}" - return format_mlir_op(op_str, shape, **kwargs), [tile_size, "i1"] + res = format_mlir_op(op_str, operand_shape, **kwargs) + + V.kernel.var_info[res] = [tile_size, "i1"] + return res, [tile_size, "i1"] else: # Integers cannot be NaN const_false = ops.constant(False, "i1") @@ -815,14 +907,27 @@ def floor(operand, *args, **kwargs): @staticmethod def sign(operand, *args, **kwargs): - dtype = V.kernel.var_info[operand][1] + tile_size, dtype = V.kernel.var_info[operand] - const_zero = ops.constant(0, dtype) - const_one = ops.constant(1, dtype) - const_neg_one = ops.constant(-1, dtype) + if dtype.startswith("f"): + v_zero, v_one, v_neg_one = 0.0, 1.0, -1.0 + else: + v_zero, v_one, v_neg_one = 0, 1, -1 + + const_zero = ops.constant(v_zero, dtype) + const_one = ops.constant(v_one, dtype) + const_neg_one = ops.constant(v_neg_one, dtype) + + if tile_size > 1: + const_zero = ops.broadcast(const_zero, tile_size) + const_one = ops.broadcast(const_one, tile_size) + const_neg_one = ops.broadcast(const_neg_one, tile_size) is_pos = ops.gt(operand, const_zero) is_neg = ops.lt(operand, const_zero) + + V.kernel.var_info[is_pos] = [tile_size, "i1"] + V.kernel.var_info[is_neg] = [tile_size, "i1"] res = ops.where(is_pos, const_one, ops.where(is_neg, const_neg_one, const_zero)) return res, V.kernel.var_info[res] diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index e70f964a..a27ad3cc 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -1,5 +1,14 @@ import torch +import os +def clear_caches(): + from torch._functorch._aot_autograd.autograd_cache import AOTAutogradCache + from torch._inductor.codecache import FxGraphCache + AOTAutogradCache.clear() + torch._dynamo.reset() + os.environ["TORCHINDUCTOR_CACHE"] = "0" + FxGraphCache.clear() + def test_result(name, out, cpu_out, rtol=1e-4, atol=1e-4): if torch.allclose(out.cpu(), cpu_out, rtol=rtol, atol=atol): message = f"|{name} Test Passed|" @@ -26,6 +35,8 @@ def run_test(name, device, fn, inputs, size_desc, rtol=1e-4, atol=1e-4): npu_inputs = [x.to(device=device) for x in inputs] cpu_inputs = [x.clone() for x in inputs] + clear_caches() + opt_fn = torch.compile(dynamic=False)(fn) res = opt_fn(*npu_inputs) out = fn(*cpu_inputs) @@ -131,8 +142,8 @@ def fmod_fn(a, b): x = torch.randn(128, 128) * 10 y = torch.randn(128, 128) * 3 + 1 run_test("Fmod", device, fmod_fn, (x, y), "128x128") - # 2. Float Scalar - run_test("Fmod", device, fmod_fn, (torch.tensor([[5.5]]), torch.tensor([[2.0]])), "1x1") + #2. Float Scalar + run_test("Fmod", device, fmod_fn, (torch.tensor([[5.5102357203957]]), torch.tensor([[2.0235825235]])), "1x1") # 3. Float Vector (Tail / Remainder - Small & Large) run_test("Fmod", device, fmod_fn, (torch.randn(15, 15) * 10, torch.randn(15, 15) * 3 + 1), "15x15") run_test("Fmod", device, fmod_fn, (torch.randn(129, 129) * 10, torch.randn(129, 129) * 3 + 1), "129x129") @@ -207,6 +218,66 @@ def hypot_fn(a, b): # 4. Broadcasting (128x1 vs 1x128) run_test("Hypot_Broadcast", device, hypot_fn, (torch.randn(128, 1), torch.randn(1, 128)), "broadcast") +def test_cosh(device): + def cosh_fn(a): + return torch.cosh(a) + + # 1. Float Vector (Aligned) + run_test("Cosh", device, cosh_fn, torch.randn(128, 128), "128x128") + # 2. Float Scalar (test large positive float) + run_test("Cosh", device, cosh_fn, torch.tensor([[4.5]]), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Cosh", device, cosh_fn, torch.randn(15, 15), "15x15") + run_test("Cosh", device, cosh_fn, torch.randn(129, 129), "129x129") + +def test_sinh(device): + def sinh_fn(a): + return torch.sinh(a) + + # 1. Float Vector (Aligned) + run_test("Sinh", device, sinh_fn, torch.randn(128, 128), "128x128") + # 2. Float Scalar (test large positive float) + run_test("Sinh", device, sinh_fn, torch.tensor([[4.5]]), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Sinh", device, sinh_fn, torch.randn(15, 15), "15x15") + run_test("Sinh", device, sinh_fn, torch.randn(129, 129), "129x129") + +def test_acosh(device): + def acosh_fn(a): + return torch.acosh(a) + + # 1. Float Vector (Aligned) + run_test("Acosh", device, acosh_fn, torch.rand(128, 128) + 1, "128x128") # Values in [1, 2] + # 2. Float Scalar + run_test("Acosh", device, acosh_fn, torch.tensor([[1.5]]), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Acosh", device, acosh_fn, torch.rand(15, 15) + 1, "15x15") + run_test("Acosh", device, acosh_fn, torch.rand(129, 129) + 1, "129x129") + +def test_asinh(device): + def asinh_fn(a): + return torch.asinh(a) + + # 1. Float Vector (Aligned) + run_test("Asinh", device, asinh_fn, torch.randn(128, 128), "128x128") + # 2. Float Scalar + run_test("Asinh", device, asinh_fn, torch.tensor([[1.5]]), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Asinh", device, asinh_fn, torch.randn(15, 15), "15x15") + run_test("Asinh", device, asinh_fn, torch.randn(129, 129), "129x129") + +def test_atanh(device): + def atanh_fn(a): + return torch.atanh(a) + + # 1. Float Vector (Aligned) + run_test("Atanh", device, atanh_fn, torch.rand(128, 128) * 2 - 1, "128x128") # Values in (-1, 1) + # 2. Float Scalar + run_test("Atanh", device, atanh_fn, torch.tensor([[0.5]]), "1x1") + # 3. Float Vector (Tail / Remainder - Small & Large) + run_test("Atanh", device, atanh_fn, torch.rand(15, 15) * 2 - 1, "15x15") + run_test("Atanh", device, atanh_fn, torch.rand(129, 129) * 2 - 1, "129x129") + def test_log(device): def log_fn(a): return torch.log(a) @@ -218,18 +289,25 @@ def log_fn(a): # 3. Float Vector (Tail / Remainder - Small & Large) run_test("Log", device, log_fn, torch.rand(15, 15) + 1e-5, "15x15") run_test("Log", device, log_fn, torch.rand(129, 129) + 1e-5, "129x129") - + if __name__ == "__main__": device = torch.device("npu:0") - test_abs(device) - test_sign(device) - test_isnan(device) - test_isinf(device) - test_fmod(device) - test_lshift(device) - test_rshift(device) - test_copysign(device) - test_erfc(device) - test_hypot(device) - test_log(device) \ No newline at end of file + # test_abs(device) + # test_sign(device) + # test_isnan(device) + # test_isinf(device) + # test_fmod(device) + # test_lshift(device) + # test_rshift(device) + # test_copysign(device) + # test_erfc(device) + # test_hypot(device) + # test_cosh(device) + # test_sinh(device) + # test_acos(device) + # test_acosh(device) + # test_asinh(device) + # test_atanh(device) + test_log(device) + \ No newline at end of file From 7649abcb2c802e13ce44a6e339cf5f3cbb9c0bee Mon Sep 17 00:00:00 2001 From: Jagggged Date: Wed, 8 Jul 2026 14:33:36 +0900 Subject: [PATCH 07/11] [Frontend] Implement atan as VCIX vector instruction --- PyTorchSimFrontend/mlir/mlir_ops.py | 16 +- gem5_script/vpu_config.py | 3 +- tests/test_pointwise.py | 332 +++++++++------------------- 3 files changed, 123 insertions(+), 228 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index a15c5a9f..4ed4022c 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -591,7 +591,21 @@ def atan2(operand1, operand2, *args, **kwargs): @staticmethod def atan(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.atan(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype + return format_mlir_op(f'math.atan %{operand}', shape, **kwargs), [tile_size, dtype] @staticmethod def atanh(operand, *args, **kwargs): diff --git a/gem5_script/vpu_config.py b/gem5_script/vpu_config.py index 329f323c..6080efd4 100644 --- a/gem5_script/vpu_config.py +++ b/gem5_script/vpu_config.py @@ -20,7 +20,6 @@ class SpecialFunctionUnit(MinorFU): "CustomVtanh", "CustomVsin", "CustomVcos", - "CustomVlog", ]) opLat = 10 @@ -121,6 +120,8 @@ class MinorVecMisc(MinorFU): "SimdExt", "SimdFloatExt", "CustomVlaneIdx", + "CustomVlog", + "CustomVatan", ] ) opLat = 1 diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index a27ad3cc..6c7daed6 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -52,262 +52,142 @@ def run_test(name, device, fn, inputs, size_desc, rtol=1e-4, atol=1e-4): test_result(f"{name}_{size_desc}", res, out, rtol=rtol, atol=atol) -def test_abs(device): - def abs_fn(a): - return torch.abs(a) - # 1. Float Vector (Aligned) - run_test("Abs_Float", device, abs_fn, torch.randn(128, 128) * 10, "128x128") - # 2. Float Scalar - run_test("Abs_Float", device, abs_fn, torch.randn(1, 1) * 10, "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Abs_Float", device, abs_fn, torch.randn(15, 15) * 10, "15x15") - run_test("Abs_Float", device, abs_fn, torch.randn(129, 129) * 10, "129x129") - # 4. Int Vector (Aligned) - run_test("Abs_Int", device, abs_fn, torch.randint(-100, 100, (128, 128), dtype=torch.int32), "128x128_int") - # 5. Non-contiguous (Transposed & Strided Sliced) - run_test("Abs_Float_Strided", device, abs_fn, (torch.randn(128, 128) * 10).t(), "128x128_strided_transposed") - run_test("Abs_Float_Strided", device, abs_fn, (torch.randn(128, 256) * 10)[:, ::2], "128x128_strided_sliced") +# aligned, scalar, small tail, large tail +STD_SIZES = [(128, 128), (1, 1), (15, 15), (129, 129)] -def test_sign(device): - def sign_fn(a): - return torch.sign(a) - # 1. Float Vector (Aligned) - x_float = torch.randn(128, 128) - x_float[x_float.abs() < 0.3] = 0.0 - run_test("Sign_Float", device, sign_fn, x_float, "128x128") - # 2. Float Scalar (includes zero and nonzero) - x_scalar = torch.tensor([[0.0]]) - run_test("Sign_Float", device, sign_fn, x_scalar, "1x1_zero") - x_scalar_non_zero = torch.tensor([[-4.5]]) - run_test("Sign_Float", device, sign_fn, x_scalar_non_zero, "1x1_nonzero") - # 3. Float Vector (Tail / Remainder - Small & Large) - x_tail = torch.randn(15, 15) - x_tail[x_tail.abs() < 0.3] = 0.0 - run_test("Sign_Float", device, sign_fn, x_tail, "15x15") - x_tail_large = torch.randn(129, 129) - x_tail_large[x_tail_large.abs() < 0.3] = 0.0 - run_test("Sign_Float", device, sign_fn, x_tail_large, "129x129") - # 4. Int Vector (Aligned) - x_int = torch.randint(-5, 5, (128, 128), dtype=torch.int32) - run_test("Sign_Int", device, sign_fn, x_int, "128x128_int") +def run_op(name, device, fn, make_input, cases=None, sizes=STD_SIZES, rtol=1e-4, atol=1e-4): + torch.manual_seed(0) + for rows, cols in sizes: + run_test(name, device, fn, make_input(rows, cols), f"{rows}x{cols}", rtol=rtol, atol=atol) + for desc, inputs in (cases or []): + run_test(name, device, fn, inputs, desc, rtol=rtol, atol=atol) -def test_isnan(device): - def isnan_fn(a): - return torch.isnan(a) - # 1. Float Vector with NaNs (Aligned) - x = torch.randn(128, 128) - x[x.abs() < 0.3] = float('nan') - run_test("IsNaN", device, isnan_fn, x, "128x128") - # 2. Float Scalar - x_scalar = torch.tensor([[float('nan')]]) - run_test("IsNaN", device, isnan_fn, x_scalar, "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - x_tail = torch.randn(15, 15) - x_tail[x_tail.abs() < 0.3] = float('nan') - run_test("IsNaN", device, isnan_fn, x_tail, "15x15") - x_tail_large = torch.randn(129, 129) - x_tail_large[x_tail_large.abs() < 0.3] = float('nan') - run_test("IsNaN", device, isnan_fn, x_tail_large, "129x129") +def test_abs(device): + run_op("Abs", device, torch.abs, lambda r, c: torch.randn(r, c) * 10, + cases=[ + ("int", torch.randint(-100, 100, (128, 128), dtype=torch.int32)), + ("strided_transposed", (torch.randn(128, 128) * 10).t()), + ("strided_sliced", (torch.randn(128, 256) * 10)[:, ::2]), + ]) -def test_isinf(device): - def isinf_fn(a): - return torch.isinf(a) +def test_sign(device): + def make(r, c): + x = torch.randn(r, c) + x[x.abs() < 0.3] = 0.0 + return x + run_op("Sign", device, torch.sign, make, + cases=[ + ("zero", torch.tensor([[0.0]])), + ("nonzero", torch.tensor([[-4.5]])), + ("int", torch.randint(-5, 5, (128, 128), dtype=torch.int32)), + ]) - # 1. Float Vector with Infs (Aligned) - x = torch.randn(128, 128) - x[x > 1.0] = float('inf') - x[x < -1.0] = float('-inf') - run_test("IsInf", device, isinf_fn, x, "128x128") - # 2. Float Scalar - x_scalar = torch.tensor([[float('-inf')]]) - run_test("IsInf", device, isinf_fn, x_scalar, "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - x_tail = torch.randn(15, 15) - x_tail[x_tail > 1.0] = float('inf') - x_tail[x_tail < -1.0] = float('-inf') - run_test("IsInf", device, isinf_fn, x_tail, "15x15") - x_tail_large = torch.randn(129, 129) - x_tail_large[x_tail_large > 1.0] = float('inf') - x_tail_large[x_tail_large < -1.0] = float('-inf') - run_test("IsInf", device, isinf_fn, x_tail_large, "129x129") +def test_isnan(device): + def make(r, c): + x = torch.randn(r, c) + x[x.abs() < 0.3] = float('nan') + return x + run_op("IsNaN", device, torch.isnan, make, + cases=[("scalar_nan", torch.tensor([[float('nan')]]))]) -def test_fmod(device): - def fmod_fn(a, b): - return torch.fmod(a, b) +def test_isinf(device): + def make(r, c): + x = torch.randn(r, c) + x[x > 1.0] = float('inf') + x[x < -1.0] = float('-inf') + return x + run_op("IsInf", device, torch.isinf, make, + cases=[("scalar_inf", torch.tensor([[float('-inf')]]))]) - # 1. Float Vector (Aligned) - x = torch.randn(128, 128) * 10 - y = torch.randn(128, 128) * 3 + 1 - run_test("Fmod", device, fmod_fn, (x, y), "128x128") - #2. Float Scalar - run_test("Fmod", device, fmod_fn, (torch.tensor([[5.5102357203957]]), torch.tensor([[2.0235825235]])), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Fmod", device, fmod_fn, (torch.randn(15, 15) * 10, torch.randn(15, 15) * 3 + 1), "15x15") - run_test("Fmod", device, fmod_fn, (torch.randn(129, 129) * 10, torch.randn(129, 129) * 3 + 1), "129x129") - # 4. Broadcasting (128x1 vs 1x128) - run_test("Fmod_Broadcast", device, fmod_fn, (torch.randn(128, 1) * 10, torch.randn(1, 128) * 3 + 1), "broadcast") +def test_fmod(device): + run_op("Fmod", device, torch.fmod, + lambda r, c: (torch.randn(r, c) * 10, torch.randn(r, c) * 3 + 1), + cases=[("broadcast", (torch.randn(128, 1) * 10, torch.randn(1, 128) * 3 + 1))]) def test_lshift(device): - def lshift_fn(a, b): - return torch.bitwise_left_shift(a, b) - - # 1. Int Vector (Aligned) - run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (128, 128), dtype=torch.int32), torch.randint(1, 5, (128, 128), dtype=torch.int32)), "128x128") - # 2. Int Scalar - run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (1, 1), dtype=torch.int32), torch.randint(1, 5, (1, 1), dtype=torch.int32)), "1x1") - # 3. Int Vector (Tail / Remainder - Small & Large) - run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (15, 15), dtype=torch.int32), torch.randint(1, 5, (15, 15), dtype=torch.int32)), "15x15") - run_test("LShift", device, lshift_fn, (torch.randint(1, 100, (129, 129), dtype=torch.int32), torch.randint(1, 5, (129, 129), dtype=torch.int32)), "129x129") - # 4. Broadcasting (128x1 vs 1x128) - run_test("LShift_Broadcast", device, lshift_fn, (torch.randint(1, 100, (128, 1), dtype=torch.int32), torch.randint(1, 5, (1, 128), dtype=torch.int32)), "broadcast") + run_op("LShift", device, torch.bitwise_left_shift, + lambda r, c: (torch.randint(1, 100, (r, c), dtype=torch.int32), + torch.randint(1, 5, (r, c), dtype=torch.int32)), + cases=[("broadcast", (torch.randint(1, 100, (128, 1), dtype=torch.int32), + torch.randint(1, 5, (1, 128), dtype=torch.int32)))]) def test_rshift(device): - def rshift_fn(a, b): - return torch.bitwise_right_shift(a, b) - - # 1. Int Vector (Aligned) - run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (128, 128), dtype=torch.int32), torch.randint(1, 5, (128, 128), dtype=torch.int32)), "128x128") - # 2. Int Scalar - run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (1, 1), dtype=torch.int32), torch.randint(1, 5, (1, 1), dtype=torch.int32)), "1x1") - # 3. Int Vector (Tail / Remainder - Small & Large) - run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (15, 15), dtype=torch.int32), torch.randint(1, 5, (15, 15), dtype=torch.int32)), "15x15") - run_test("RShift", device, rshift_fn, (torch.randint(10, 1000, (129, 129), dtype=torch.int32), torch.randint(1, 5, (129, 129), dtype=torch.int32)), "129x129") - # 4. Broadcasting (128x1 vs 1x128) - run_test("RShift_Broadcast", device, rshift_fn, (torch.randint(10, 1000, (128, 1), dtype=torch.int32), torch.randint(1, 5, (1, 128), dtype=torch.int32)), "broadcast") + run_op("RShift", device, torch.bitwise_right_shift, + lambda r, c: (torch.randint(10, 1000, (r, c), dtype=torch.int32), + torch.randint(1, 5, (r, c), dtype=torch.int32)), + cases=[("broadcast", (torch.randint(10, 1000, (128, 1), dtype=torch.int32), + torch.randint(1, 5, (1, 128), dtype=torch.int32)))]) def test_copysign(device): - def copysign_fn(a, b): - return torch.copysign(a, b) - - # 1. Float Vector (Aligned) - run_test("Copysign", device, copysign_fn, (torch.randn(128, 128) * 10, torch.randn(128, 128)), "128x128") - # 2. Float Scalar (test sign of zero case specifically: negative zero) - run_test("Copysign", device, copysign_fn, (torch.tensor([[3.0]]), torch.tensor([[-0.0]])), "1x1_negzero") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Copysign", device, copysign_fn, (torch.randn(15, 15) * 10, torch.randn(15, 15)), "15x15") - run_test("Copysign", device, copysign_fn, (torch.randn(129, 129) * 10, torch.randn(129, 129)), "129x129") - # 4. Broadcasting (128x1 vs 1x128) - run_test("Copysign_Broadcast", device, copysign_fn, (torch.randn(128, 1) * 10, torch.randn(1, 128)), "broadcast") + run_op("Copysign", device, torch.copysign, + lambda r, c: (torch.randn(r, c) * 10, torch.randn(r, c)), + cases=[ + ("negzero", (torch.tensor([[3.0]]), torch.tensor([[-0.0]]))), + ("broadcast", (torch.randn(128, 1) * 10, torch.randn(1, 128))), + ]) def test_erfc(device): - def erfc_fn(a): - return torch.erfc(a) - - # 1. Float Vector (Aligned) - run_test("Erfc", device, erfc_fn, torch.randn(128, 128), "128x128") - # 2. Float Scalar (test large positive float) - run_test("Erfc", device, erfc_fn, torch.tensor([[4.5]]), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Erfc", device, erfc_fn, torch.randn(15, 15), "15x15") - run_test("Erfc", device, erfc_fn, torch.randn(129, 129), "129x129") + run_op("Erfc", device, torch.erfc, lambda r, c: torch.randn(r, c), + cases=[("large", torch.tensor([[4.5]]))]) def test_hypot(device): - def hypot_fn(a, b): - return torch.hypot(a, b) - - # 1. Float Vector (Aligned) - run_test("Hypot", device, hypot_fn, (torch.randn(128, 128), torch.randn(128, 128)), "128x128") - # 2. Float Scalar (Result should be exactly 5.0) - run_test("Hypot", device, hypot_fn, (torch.tensor([[3.0]]), torch.tensor([[4.0]])), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Hypot", device, hypot_fn, (torch.randn(15, 15), torch.randn(15, 15)), "15x15") - run_test("Hypot", device, hypot_fn, (torch.randn(129, 129), torch.randn(129, 129)), "129x129") - # 4. Broadcasting (128x1 vs 1x128) - run_test("Hypot_Broadcast", device, hypot_fn, (torch.randn(128, 1), torch.randn(1, 128)), "broadcast") + run_op("Hypot", device, torch.hypot, + lambda r, c: (torch.randn(r, c), torch.randn(r, c)), + cases=[ + ("3-4-5", (torch.tensor([[3.0]]), torch.tensor([[4.0]]))), + ("broadcast", (torch.randn(128, 1), torch.randn(1, 128))), + ]) def test_cosh(device): - def cosh_fn(a): - return torch.cosh(a) - - # 1. Float Vector (Aligned) - run_test("Cosh", device, cosh_fn, torch.randn(128, 128), "128x128") - # 2. Float Scalar (test large positive float) - run_test("Cosh", device, cosh_fn, torch.tensor([[4.5]]), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Cosh", device, cosh_fn, torch.randn(15, 15), "15x15") - run_test("Cosh", device, cosh_fn, torch.randn(129, 129), "129x129") + run_op("Cosh", device, torch.cosh, lambda r, c: torch.randn(r, c), + cases=[("large", torch.tensor([[4.5]]))]) def test_sinh(device): - def sinh_fn(a): - return torch.sinh(a) + run_op("Sinh", device, torch.sinh, lambda r, c: torch.randn(r, c), + cases=[("large", torch.tensor([[4.5]]))]) - # 1. Float Vector (Aligned) - run_test("Sinh", device, sinh_fn, torch.randn(128, 128), "128x128") - # 2. Float Scalar (test large positive float) - run_test("Sinh", device, sinh_fn, torch.tensor([[4.5]]), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Sinh", device, sinh_fn, torch.randn(15, 15), "15x15") - run_test("Sinh", device, sinh_fn, torch.randn(129, 129), "129x129") - -def test_acosh(device): - def acosh_fn(a): - return torch.acosh(a) +def test_log(device): + # domain: x > 0 + run_op("Log", device, torch.log, lambda r, c: torch.rand(r, c) + 1e-5, + cases=[("e", torch.tensor([[2.71828]]))]) - # 1. Float Vector (Aligned) - run_test("Acosh", device, acosh_fn, torch.rand(128, 128) + 1, "128x128") # Values in [1, 2] - # 2. Float Scalar - run_test("Acosh", device, acosh_fn, torch.tensor([[1.5]]), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Acosh", device, acosh_fn, torch.rand(15, 15) + 1, "15x15") - run_test("Acosh", device, acosh_fn, torch.rand(129, 129) + 1, "129x129") +def test_acosh(device): + # domain: x >= 1 + run_op("Acosh", device, torch.acosh, lambda r, c: torch.rand(r, c) + 1, + cases=[("one", torch.tensor([[1.0]]))]) def test_asinh(device): - def asinh_fn(a): - return torch.asinh(a) - - # 1. Float Vector (Aligned) - run_test("Asinh", device, asinh_fn, torch.randn(128, 128), "128x128") - # 2. Float Scalar - run_test("Asinh", device, asinh_fn, torch.tensor([[1.5]]), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Asinh", device, asinh_fn, torch.randn(15, 15), "15x15") - run_test("Asinh", device, asinh_fn, torch.randn(129, 129), "129x129") + run_op("Asinh", device, torch.asinh, lambda r, c: torch.randn(r, c)) def test_atanh(device): - def atanh_fn(a): - return torch.atanh(a) - - # 1. Float Vector (Aligned) - run_test("Atanh", device, atanh_fn, torch.rand(128, 128) * 2 - 1, "128x128") # Values in (-1, 1) - # 2. Float Scalar - run_test("Atanh", device, atanh_fn, torch.tensor([[0.5]]), "1x1") - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Atanh", device, atanh_fn, torch.rand(15, 15) * 2 - 1, "15x15") - run_test("Atanh", device, atanh_fn, torch.rand(129, 129) * 2 - 1, "129x129") - -def test_log(device): - def log_fn(a): - return torch.log(a) - - # 1. Float Vector (Aligned) - run_test("Log", device, log_fn, torch.rand(128, 128) + 1e-5, "128x128") # Avoid log(0) - # 2. Float Scalar - run_test("Log", device, log_fn, torch.tensor([[2.71828]]), "1x1") # log(e) = 1 - # 3. Float Vector (Tail / Remainder - Small & Large) - run_test("Log", device, log_fn, torch.rand(15, 15) + 1e-5, "15x15") - run_test("Log", device, log_fn, torch.rand(129, 129) + 1e-5, "129x129") - + # domain: (-1, 1) + run_op("Atanh", device, torch.atanh, lambda r, c: torch.rand(r, c) * 2 - 1) + +def test_atan(device): + # domain: all reals; boundary: zero, unit, large |x| (range-reduction path) + run_op("Atan", device, torch.atan, lambda r, c: torch.randn(r, c) * 5, + cases=[("boundary", torch.tensor([[0.0, 1.0, -1.0, 1e3, -1e3, 1e-4]]))]) + if __name__ == "__main__": device = torch.device("npu:0") - # test_abs(device) - # test_sign(device) - # test_isnan(device) - # test_isinf(device) - # test_fmod(device) - # test_lshift(device) - # test_rshift(device) - # test_copysign(device) - # test_erfc(device) - # test_hypot(device) - # test_cosh(device) - # test_sinh(device) - # test_acos(device) - # test_acosh(device) - # test_asinh(device) - # test_atanh(device) + test_abs(device) + test_sign(device) + test_isnan(device) + test_isinf(device) + test_fmod(device) + test_lshift(device) + test_rshift(device) + test_copysign(device) + test_erfc(device) + test_hypot(device) + test_cosh(device) + test_sinh(device) test_log(device) - \ No newline at end of file + test_acosh(device) + test_asinh(device) + test_atanh(device) + test_atan(device) \ No newline at end of file From bff874eb7b761e1b264b9f3231746b37ddd5a254 Mon Sep 17 00:00:00 2001 From: Jagggged Date: Wed, 8 Jul 2026 14:34:03 +0900 Subject: [PATCH 08/11] [Frontend] Implement asin/acos/atan2 via atan decomposition --- PyTorchSimFrontend/mlir/mlir_ops.py | 49 +++++++++++++++++++++++++++-- tests/test_pointwise.py | 23 +++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index 4ed4022c..62719cd3 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -533,7 +533,22 @@ def tanh(operand, *args, **kwargs): @staticmethod def acos(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.acos(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + asin_val = ops.asin(operand) + res = ops.sub(ops.constant(math.pi / 2, dtype), asin_val) + return res, V.kernel.var_info[res] @staticmethod def acosh(operand, *args, **kwargs): @@ -560,7 +575,23 @@ def acosh(operand, *args, **kwargs): @staticmethod def asin(operand, *args, **kwargs): - raise NotImplementedError + tile_size, dtype = V.kernel.var_info[operand] + + # Check scalar + if tile_size == 1: + operand = ops.broadcast(operand, 4) + val = ops.asin(operand) + result = ops.extractelement(val, 0) + return result, V.kernel.var_info[result] + + if dtype.startswith("f"): + operand = ops.to_dtype(operand, "f32") + dtype = "f32" + + x2 = ops.square(operand) + denom = ops.sqrt(ops.sub(ops.constant(1.0, dtype), x2)) + res = ops.atan(ops.truediv(operand, denom)) + return res, V.kernel.var_info[res] @staticmethod def asinh(operand, *args, **kwargs): @@ -587,7 +618,19 @@ def asinh(operand, *args, **kwargs): @staticmethod def atan2(operand1, operand2, *args, **kwargs): - raise NotImplementedError + tile_size, ret_type, y, x = ExtensionOverrides.binary_elementwise_common(operand1, operand2) + if not ret_type.startswith("f"): + y = ops.to_dtype(y, "f32") + x = ops.to_dtype(x, "f32") + ret_type = "f32" + + pi = ops.constant(math.pi, ret_type) + zero = ops.constant(0.0, ret_type) + + base = ops.atan(ops.truediv(y, x)) + corrected = ops.add(base, ops.copysign(pi, y)) + res = ops.where(ops.lt(x, zero), corrected, base) + return res, [tile_size, ret_type] @staticmethod def atan(operand, *args, **kwargs): diff --git a/tests/test_pointwise.py b/tests/test_pointwise.py index 6c7daed6..74e6656d 100644 --- a/tests/test_pointwise.py +++ b/tests/test_pointwise.py @@ -170,6 +170,24 @@ def test_atan(device): # domain: all reals; boundary: zero, unit, large |x| (range-reduction path) run_op("Atan", device, torch.atan, lambda r, c: torch.randn(r, c) * 5, cases=[("boundary", torch.tensor([[0.0, 1.0, -1.0, 1e3, -1e3, 1e-4]]))]) + +def test_asin(device): + # domain: [-1, 1]; boundary includes the +/-1 endpoints + run_op("Asin", device, torch.asin, lambda r, c: torch.rand(r, c) * 2 - 1, + cases=[("boundary", torch.tensor([[0.0, 1.0, -1.0, 0.5, -0.5]]))]) + +def test_acos(device): + # domain: [-1, 1]; boundary includes the +/-1 endpoints + run_op("Acos", device, torch.acos, lambda r, c: torch.rand(r, c) * 2 - 1, + cases=[("boundary", torch.tensor([[0.0, 1.0, -1.0, 0.5, -0.5]]))]) + +def test_atan2(device): + # atan2(y, x); boundary: axes and diagonals across all four quadrants + # (0, 0) excluded: composition yields atan(0/0)=nan while torch returns 0 + y = torch.tensor([[1.0, 0.0, -1.0, 0.0, 1.0, -1.0, 1.0, -1.0]]) + x = torch.tensor([[0.0, 1.0, 0.0, -1.0, 1.0, -1.0, -1.0, 1.0]]) + run_op("Atan2", device, torch.atan2, lambda r, c: (torch.randn(r, c), torch.randn(r, c)), + cases=[("boundary", (y, x))]) if __name__ == "__main__": device = torch.device("npu:0") @@ -190,4 +208,7 @@ def test_atan(device): test_acosh(device) test_asinh(device) test_atanh(device) - test_atan(device) \ No newline at end of file + test_atan(device) + test_asin(device) + test_acos(device) + test_atan2(device) \ No newline at end of file From 77ee26d19b340df2b13a1f143957700f278c8389 Mon Sep 17 00:00:00 2001 From: Wonhyuk Yang Date: Wed, 8 Jul 2026 22:17:56 +0900 Subject: [PATCH 09/11] [Frontend] Cast non-float inputs to f32 for float-only pointwise ops The transcendental ops (log, atan, atanh, cosh, sinh, acos, asin, acosh, asinh, tanh) lower to float-only instructions. Promote non-float inputs (integers) to f32 to run them while leaving native float widths (f16/f64) untouched, so the ops stay dtype-preserving for floats. Also drop a leftover isnan debug print. --- PyTorchSimFrontend/mlir/mlir_ops.py | 43 +++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/PyTorchSimFrontend/mlir/mlir_ops.py b/PyTorchSimFrontend/mlir/mlir_ops.py index 62719cd3..cc23a953 100644 --- a/PyTorchSimFrontend/mlir/mlir_ops.py +++ b/PyTorchSimFrontend/mlir/mlir_ops.py @@ -475,7 +475,9 @@ def cosh(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -499,7 +501,9 @@ def sinh(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -525,9 +529,11 @@ def tanh(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - # Type check & auto cast - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") + dtype = "f32" shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype return format_mlir_op(f'math.tanh %{operand}', shape, **kwargs), [tile_size, dtype] @@ -542,7 +548,9 @@ def acos(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -561,7 +569,9 @@ def acosh(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -584,7 +594,9 @@ def asin(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -604,7 +616,9 @@ def asinh(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -643,7 +657,9 @@ def atan(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -661,7 +677,9 @@ def atanh(operand, *args, **kwargs): result = ops.extractelement(val, 0) return result, V.kernel.var_info[result] - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -761,7 +779,9 @@ def log(operand, *args, **kwargs): dtype = op_type[1] # Type check & auto cast - if dtype.startswith("f"): + # Float-only instruction: promote non-float inputs (e.g. integers) to f32 + # to run it. Native float widths (f16/f64) are left untouched. + if not dtype.startswith("f"): operand = ops.to_dtype(operand, "f32") dtype = "f32" @@ -924,7 +944,6 @@ def isinf(operand, *args, **kwargs): @staticmethod def isnan(operand, *args, **kwargs): tile_size, dtype = V.kernel.var_info[operand] - print(f"Checking isnan for operand with dtype: {dtype} and tile_size: {tile_size}") if dtype.startswith("f"): # Unordered comparison (uno) to detect NaN (uno returns true if either operand is NaN) operand_shape = f"vector<{tile_size}x{dtype}>" if tile_size > 1 else dtype From 7de83cf7fcf71c2f1ed8df5919c279e65891ebb5 Mon Sep 17 00:00:00 2001 From: Wonhyuk Yang Date: Wed, 8 Jul 2026 22:17:56 +0900 Subject: [PATCH 10/11] [CI] Add test_pointwise.py to the test workflow Register the new pointwise op test in the explicit allowlist so it gates PRs like the other tests. --- .github/workflows/pytorchsim_test.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/pytorchsim_test.yml b/.github/workflows/pytorchsim_test.yml index 4b4fab80..d2c48e62 100644 --- a/.github/workflows/pytorchsim_test.yml +++ b/.github/workflows/pytorchsim_test.yml @@ -54,6 +54,25 @@ jobs: -e vpu_spad_size_kb_per_lane="${{ inputs.spad_size }}" \ ${{ inputs.image_name }} python3 PyTorchSim/tests/test_transcendental.py + test_pointwise: + name: Run test_pointwise.py + runs-on: self-hosted + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run test_pointwise.py + run: | + echo "Running test_pointwise.py" + docker run --rm \ + -e vpu_num_lanes="${{ inputs.vector_lane }}" \ + -e vpu_spad_size_kb_per_lane="${{ inputs.spad_size }}" \ + ${{ inputs.image_name }} python3 PyTorchSim/tests/test_pointwise.py + test_activation: name: Run test_activation.py runs-on: self-hosted From dd2b3348fd8b88b45e4872b37021eb2aec39507e Mon Sep 17 00:00:00 2001 From: Wonhyuk Yang Date: Wed, 8 Jul 2026 22:30:45 +0900 Subject: [PATCH 11/11] [Build] Pin spike v1.0.6 and gem5 v1.0.2 for the pointwise ops Bump the third-party pins to the releases that ship the new pointwise support: spike v1.0.6 (torchsim_vlog/vatan) and gem5 v1.0.2 (CustomVlog/ Vatan opclasses plus vsin/vcos/vlog/vatan decode), so CI and the base image pick them up. --- thirdparty/github-releases.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/thirdparty/github-releases.json b/thirdparty/github-releases.json index ec89c24f..bc203351 100644 --- a/thirdparty/github-releases.json +++ b/thirdparty/github-releases.json @@ -3,7 +3,7 @@ "pytorch_image": "pytorch/pytorch:2.8.0-cuda12.6-cudnn9-devel", "gem5": { "repository": "PSAL-POSTECH/gem5", - "release_tag": "v1.0.1", + "release_tag": "v1.0.2", "asset_name": "gem5-release.tar.gz" }, "llvm_project": { @@ -13,7 +13,7 @@ }, "spike": { "repository": "PSAL-POSTECH/riscv-isa-sim", - "release_tag": "v1.0.1", + "release_tag": "v1.0.6", "asset_name": "spike-release.tar.gz" } }