Skip to content

Commit fb80db1

Browse files
authored
Merge a33ac61 into 17769b8
2 parents 17769b8 + a33ac61 commit fb80db1

5 files changed

Lines changed: 173 additions & 48 deletions

File tree

aie_kernels/aie2p/gelu.cc

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,58 @@
88

99
using namespace aie;
1010

11-
void gelu_tanh_approx_bf16(bfloat16 *restrict input_vector, bfloat16 *restrict output_vector, const int32_t vector_size)
11+
// One 16-lane GELU (tanh approximation): 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))).
12+
static inline aie::vector<bfloat16, 16>
13+
gelu_tanh_approx_v16(aie::vector<bfloat16, 16> x)
1214
{
13-
event0();
14-
15-
auto it_in = aie::begin_restrict_vector<16>((bfloat16 *)input_vector);
16-
auto it_out = aie::begin_restrict_vector<16>((bfloat16 *)output_vector);
17-
18-
aie::vector<bfloat16, 16> input;
19-
20-
// Constants
2115
const bfloat16 k0_5 = 0.5f;
2216
const bfloat16 k1 = 1.0f;
23-
const bfloat16 sqrt_2_over_pi = 0.79788456f; // sqrt(2/π)
17+
const bfloat16 sqrt_2_over_pi = 0.79788456f; // sqrt(2/pi)
2418
const bfloat16 kBeta = 0.044715f;
2519

2620
auto v05 = aie::broadcast<bfloat16, 16>(k0_5);
2721
auto v1 = aie::broadcast<bfloat16, 16>(k1);
2822
auto vs2opi = aie::broadcast<bfloat16, 16>(sqrt_2_over_pi);
2923
auto vBeta = aie::broadcast<bfloat16, 16>(kBeta);
3024

25+
aie::vector<bfloat16, 16> x2 = aie::mul(x, x);
26+
aie::vector<bfloat16, 16> x3 = aie::mul(x, x2);
27+
aie::vector<bfloat16, 16> x3_beta = aie::mul(x3, vBeta);
28+
aie::vector<bfloat16, 16> inner = aie::add(x, x3_beta);
29+
auto inner1 = aie::mul(inner, vs2opi);
30+
auto tanh_out = aie::tanh<bfloat16>(inner1.to_vector<float>());
31+
aie::vector<bfloat16, 16> one_plus_tanh = aie::add(tanh_out, v1);
32+
aie::vector<bfloat16, 16> mul_v05 = aie::mul(v05, one_plus_tanh);
33+
return aie::mul(x, mul_v05).to_vector<bfloat16>();
34+
}
35+
36+
// Out-of-place GELU: output_vector = gelu(input_vector). input and output must not alias.
37+
void gelu_tanh_approx_bf16(bfloat16 *restrict input_vector, bfloat16 *restrict output_vector, const int32_t vector_size)
38+
{
39+
event0();
40+
auto it_in = aie::begin_restrict_vector<16>((bfloat16 *)input_vector);
41+
auto it_out = aie::begin_restrict_vector<16>((bfloat16 *)output_vector);
42+
3143
AIE_PREPARE_FOR_PIPELINING
3244
AIE_LOOP_MIN_ITERATION_COUNT(64)
3345
for (int i = 0; i < vector_size; i += 16) {
34-
input = *it_in++;
35-
auto x = input;
36-
37-
// Compute x^3
38-
aie::vector<bfloat16, 16> x2 = aie::mul(x, x); // x^2
39-
aie::vector<bfloat16, 16> x3 = aie::mul(x, x2); // x^3
40-
41-
// inner = sqrt(2/pi) * (x + 0.044715 * x^3)
42-
aie::vector<bfloat16, 16> x3_beta = aie::mul(x3, vBeta);
43-
aie::vector<bfloat16, 16> inner = aie::add(x, x3_beta);
44-
auto inner1 = aie::mul(inner, vs2opi);
45-
46-
// tanh_out = tanh(inner)
47-
auto tanh_out = aie::tanh<bfloat16>(inner1.to_vector<float>());
48-
49-
// result = 0.5 * x * (1 + tanh_out)
50-
aie::vector<bfloat16, 16> one_plus_tanh = aie::add(tanh_out, v1);
51-
// Multiply by x and 0.5
52-
aie::vector<bfloat16, 16> mul_v05 = aie::mul(v05, one_plus_tanh);
53-
auto result = aie::mul(x, mul_v05);
54-
55-
*it_out++ = result.to_vector<bfloat16>();
46+
*it_out++ = gelu_tanh_approx_v16(*it_in++);
5647
}
57-
5848
event1();
49+
}
5950

60-
return;
51+
// In-place GELU: v = gelu(v). Single pointer, so aliasing-correct (each 16-lane slot is read then written).
52+
static inline void gelu_tanh_approx_inplace_bf16(bfloat16 *restrict v, const int32_t vector_size)
53+
{
54+
event0();
55+
auto it = aie::begin_restrict_vector<16>(v);
56+
AIE_PREPARE_FOR_PIPELINING
57+
AIE_LOOP_MIN_ITERATION_COUNT(64)
58+
for (int i = 0; i < vector_size; i += 16) {
59+
aie::vector<bfloat16, 16> x = *it;
60+
*it++ = gelu_tanh_approx_v16(x);
61+
}
62+
event1();
6163
}
6264

6365
extern "C" {
@@ -67,4 +69,11 @@ void gelu_bf16(bfloat16 *restrict input, bfloat16 *restrict output, int input_si
6769
gelu_tanh_approx_bf16(input, output, input_size);
6870
}
6971

72+
// In-place GELU over n bf16 elements (n a multiple of 16). Intended as a fused epilogue over a compute
73+
// tile (e.g. a GEMV output tile), applied once per tile in the producing core.
74+
void gelu_tile_bf16(uint32_t n, bfloat16 *restrict c)
75+
{
76+
gelu_tanh_approx_inplace_bf16(c, (int32_t)n);
77+
}
78+
7079
} // extern "C"

iron/operators/gemv/design.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def my_matvec(
3636
kernel_object="mv.o",
3737
func_prefix="",
3838
verbose=False,
39+
epilogue="none",
3940
):
4041
if m_output is None:
4142
m_output = m_input
@@ -85,6 +86,18 @@ def my_matvec(
8586
f"{func_prefix}{kernel_object}",
8687
[np.int32, np.int32, L1_A_ty, L1_B_ty, L1_C_ty],
8788
)
89+
# Optional fused activation over the full m_output C-tile, applied once per tile in core_body
90+
# (after the matvec inner-loop has filled all rows) rather than per matvec call, whose m_input
91+
# tile can be smaller than the 16-wide activation vector.
92+
assert epilogue in ("none", "gelu")
93+
gelu_kernel = None
94+
if epilogue == "gelu":
95+
assert m_output % 16 == 0, f"gelu epilogue needs m_output % 16 == 0 (got {m_output})"
96+
gelu_kernel = Kernel(
97+
f"{func_prefix}gelu_tile_bf16",
98+
f"{func_prefix}{kernel_object}",
99+
[np.int32, L1_C_ty],
100+
)
88101

89102
A_L3L1_fifos = [
90103
ObjectFifo(L1_A_ty, name=f"A_L3L1_{i}", depth=2) for i in range(cols)
@@ -96,7 +109,7 @@ def my_matvec(
96109
ObjectFifo(L1_C_ty, name=f"C_L1L3_{i}", depth=2) for i in range(cols)
97110
]
98111

99-
def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec):
112+
def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec, gelu_kernel=None):
100113
one_idx = index.constant(1)
101114
for _ in range_(0xFFFFFFFF): # batch dim handled as part of this loop
102115
b = B_L3L1_fifo.acquire(1)
@@ -110,6 +123,8 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec):
110123
a = A_L3L1_fifo.acquire(1)
111124
matvec(m_input, output_row_offset, a, b, c)
112125
A_L3L1_fifo.release(1)
126+
if gelu_kernel is not None:
127+
gelu_kernel(m_output, c)
113128
C_L1L3_fifo.release(1)
114129
B_L3L1_fifo.release(1)
115130

@@ -121,7 +136,8 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec):
121136
B_L3L1_fifos[i].cons(),
122137
C_L1L3_fifos[i].prod(),
123138
matvec,
124-
],
139+
]
140+
+ ([gelu_kernel] if epilogue == "gelu" else []),
125141
)
126142
for i in range(cols)
127143
]

iron/operators/gemv/op.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
MLIROperator,
99
AIERuntimeArgSpec,
1010
KernelObjectArtifact,
11+
KernelArchiveArtifact,
1112
SourceArtifact,
1213
PythonGeneratedMLIRArtifact,
1314
DesignGenerator,
1415
)
1516
import aie.utils as aie_utils
17+
from iron.common.device_utils import get_kernel_dir
1618

1719

1820
@dataclass
@@ -26,6 +28,10 @@ class GEMV(MLIROperator):
2628
tile_size_output: int | None = None
2729
num_batches: int = 1
2830
kernel_vector_size: int = field(default=64, repr=False)
31+
# Optional fused activation applied to each output tile in the producing core.
32+
# "none" (default) leaves the output unchanged; "gelu" applies GELU(tanh approx).
33+
# repr=False keeps operator/artifact names stable for the default path.
34+
epilogue: str = field(default="none", repr=False)
2935
context: object = field(default=None, repr=False)
3036

3137
_name_aliases: ClassVar[Dict[str, str]] = {
@@ -49,9 +55,23 @@ def __post_init__(self):
4955
self.K >= self.kernel_vector_size and self.K % self.kernel_vector_size == 0
5056
):
5157
raise ValueError("K must be multiple of kernel_vector_size")
58+
if self.epilogue not in ("none", "gelu"):
59+
raise ValueError(f"unknown epilogue {self.epilogue!r} (expected 'none' or 'gelu')")
60+
if self.epilogue == "gelu" and self.tile_size_output % 16 != 0:
61+
raise ValueError(
62+
f"gelu epilogue needs tile_size_output % 16 == 0 (got {self.tile_size_output})"
63+
)
5264

5365
MLIROperator.__init__(self, context=self.context)
5466

67+
@property
68+
def _kernel_link_file(self):
69+
# With the gelu epilogue the core also links the gelu kernel, so the object becomes an
70+
# archive of (matvec, gelu); the plain matvec stays a single object.
71+
if self.epilogue == "gelu":
72+
return f"gemv_{self.K}k_{self.kernel_vector_size}vs_gelu_kernels.a"
73+
return f"gemv_{self.K}k_{self.kernel_vector_size}vs.o"
74+
5575
def get_mlir_artifact(self):
5676
mlir_verbose = getattr(self.context, "mlir_verbose", False)
5777

@@ -71,26 +91,46 @@ def get_mlir_artifact(self):
7191
),
7292
{
7393
"verbose": mlir_verbose,
74-
"kernel_object": f"gemv_{self.K}k_{self.kernel_vector_size}vs.o",
94+
"kernel_object": self._kernel_link_file,
95+
"epilogue": self.epilogue,
7596
},
7697
),
7798
)
7899

79100
def get_kernel_artifacts(self):
80-
return [
81-
KernelObjectArtifact(
82-
f"gemv_{self.K}k_{self.kernel_vector_size}vs.o",
101+
matvec_obj = KernelObjectArtifact(
102+
f"gemv_{self.K}k_{self.kernel_vector_size}vs.o",
103+
dependencies=[
104+
SourceArtifact(
105+
self.context.base_dir / "aie_kernels" / "generic" / "mv.cc"
106+
)
107+
],
108+
extra_flags=[
109+
f"-DDIM_K={self.K}",
110+
f"-DVEC_SIZE={self.kernel_vector_size}",
111+
],
112+
)
113+
if self.epilogue == "gelu":
114+
# The gelu kernel lives in aie2p/gelu.cc, so the fused epilogue is NPU2-only.
115+
if get_kernel_dir() != "aie2p":
116+
raise NotImplementedError(
117+
"gemv gelu epilogue is only available on NPU2 (aie2p); "
118+
f"current kernel dir is {get_kernel_dir()!r}"
119+
)
120+
gelu_obj = KernelObjectArtifact(
121+
"gelu.o",
83122
dependencies=[
84123
SourceArtifact(
85-
self.context.base_dir / "aie_kernels" / "generic" / "mv.cc"
124+
self.context.base_dir / "aie_kernels" / "aie2p" / "gelu.cc"
86125
)
87126
],
88-
extra_flags=[
89-
f"-DDIM_K={self.K}",
90-
f"-DVEC_SIZE={self.kernel_vector_size}",
91-
],
92-
),
93-
]
127+
)
128+
return [
129+
KernelArchiveArtifact(
130+
self._kernel_link_file, dependencies=[matvec_obj, gelu_obj]
131+
)
132+
]
133+
return [matvec_obj]
94134

95135
def get_arg_spec(self):
96136
batch_dim = (self.num_batches,) if self.num_batches > 1 else ()

iron/operators/gemv/reference.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,13 @@ def generate_golden_reference(
4040
"B": B,
4141
"C": C,
4242
}
43+
44+
45+
def gelu_tanh_approx(x):
46+
"""Tanh-approximation GELU, matching aie_kernels/aie2p/gelu.cc.
47+
48+
0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))). Computed in float32.
49+
"""
50+
xf = np.asarray(x, dtype=np.float32)
51+
inner = 0.79788456 * (xf + 0.044715 * xf**3)
52+
return 0.5 * xf * (1.0 + np.tanh(inner))

iron/operators/gemv/test.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import aie.utils as aie_utils
77

88
from iron.operators.gemv.op import GEMV
9-
from iron.operators.gemv.reference import generate_golden_reference
9+
from iron.operators.gemv.reference import generate_golden_reference, gelu_tanh_approx
10+
from iron.common.device_utils import get_kernel_dir
11+
import numpy as np
12+
import torch
1013
from iron.common.test_utils import run_test
1114

1215

@@ -69,3 +72,50 @@ def test_gemv(M, K, num_aie_columns, tile_size_input, tile_size_output, aie_cont
6972
print(f"Effective Bandwidth: {bandwidth_gbps:.6e} GB/s\n")
7073

7174
assert not errors, f"Test failed with errors: {errors}"
75+
76+
77+
@pytest.mark.metrics(
78+
Latency=r"Latency \(us\): (?P<value>[\d\.]+)",
79+
Bandwidth=r"Effective Bandwidth: (?P<value>[\d\.e\+-]+) GB/s",
80+
Throughput=r"Throughput: (?P<value>[\d\.e\+-]+) GFLOP/s",
81+
)
82+
@pytest.mark.parametrize(
83+
"M,K,num_aie_columns,tile_size_input,tile_size_output",
84+
[
85+
pytest.param(128, 128, 1, 32, 128),
86+
pytest.param(2048, 8192, 1, 1, 2048),
87+
pytest.param(8192, 2048, 1, 4, 1024),
88+
],
89+
)
90+
def test_gemv_gelu(M, K, num_aie_columns, tile_size_input, tile_size_output, aie_context):
91+
"""GEMV with the fused GELU epilogue (NPU2-only) vs a gelu(A @ B) golden."""
92+
if get_kernel_dir() != "aie2p":
93+
pytest.skip("gemv gelu epilogue is only available on NPU2 (aie2p)")
94+
95+
golden_ref = generate_golden_reference(M=M, K=K)
96+
c_ref = golden_ref["C"].to(torch.float32).numpy()
97+
c_gelu = torch.from_numpy(gelu_tanh_approx(c_ref).astype(np.float32)).to(torch.bfloat16)
98+
99+
operator = GEMV(
100+
M=M,
101+
K=K,
102+
num_aie_columns=num_aie_columns,
103+
tile_size_input=tile_size_input,
104+
tile_size_output=tile_size_output,
105+
epilogue="gelu",
106+
context=aie_context,
107+
)
108+
109+
input_buffers = {"matrix": golden_ref["A"].flatten(), "vector": golden_ref["B"]}
110+
output_buffers = {"output": c_gelu}
111+
112+
errors, latency_us, bandwidth_gbps = run_test(
113+
operator, input_buffers, output_buffers, rel_tol=0.06, abs_tol=2e-2
114+
)
115+
116+
print(f"\nLatency: {latency_us:.1f} us")
117+
gflops = (2.0 * M * K) / (latency_us * 1e-6) / 1e9
118+
print(f"Throughput: {gflops:.6e} GFLOP/s")
119+
print(f"Effective Bandwidth: {bandwidth_gbps:.6e} GB/s\n")
120+
121+
assert not errors, f"Test failed with errors: {errors}"

0 commit comments

Comments
 (0)