Skip to content

Commit 3c991ad

Browse files
authored
Merge 93d59b5 into e216c2f
2 parents e216c2f + 93d59b5 commit 3c991ad

5 files changed

Lines changed: 180 additions & 47 deletions

File tree

aie_kernels/aie2p/gelu.cc

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,57 @@
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> gelu_tanh_approx_v16(aie::vector<bfloat16, 16> x)
1213
{
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
2114
const bfloat16 k0_5 = 0.5f;
2215
const bfloat16 k1 = 1.0f;
23-
const bfloat16 sqrt_2_over_pi = 0.79788456f; // sqrt(2/π)
16+
const bfloat16 sqrt_2_over_pi = 0.79788456f; // sqrt(2/pi)
2417
const bfloat16 kBeta = 0.044715f;
2518

2619
auto v05 = aie::broadcast<bfloat16, 16>(k0_5);
2720
auto v1 = aie::broadcast<bfloat16, 16>(k1);
2821
auto vs2opi = aie::broadcast<bfloat16, 16>(sqrt_2_over_pi);
2922
auto vBeta = aie::broadcast<bfloat16, 16>(kBeta);
3023

24+
aie::vector<bfloat16, 16> x2 = aie::mul(x, x);
25+
aie::vector<bfloat16, 16> x3 = aie::mul(x, x2);
26+
aie::vector<bfloat16, 16> x3_beta = aie::mul(x3, vBeta);
27+
aie::vector<bfloat16, 16> inner = aie::add(x, x3_beta);
28+
auto inner1 = aie::mul(inner, vs2opi);
29+
auto tanh_out = aie::tanh<bfloat16>(inner1.to_vector<float>());
30+
aie::vector<bfloat16, 16> one_plus_tanh = aie::add(tanh_out, v1);
31+
aie::vector<bfloat16, 16> mul_v05 = aie::mul(v05, one_plus_tanh);
32+
return aie::mul(x, mul_v05).to_vector<bfloat16>();
33+
}
34+
35+
// Out-of-place GELU: output_vector = gelu(input_vector). input and output must not alias.
36+
void gelu_tanh_approx_bf16(bfloat16 *restrict input_vector, bfloat16 *restrict output_vector, const int32_t vector_size)
37+
{
38+
event0();
39+
auto it_in = aie::begin_restrict_vector<16>((bfloat16 *)input_vector);
40+
auto it_out = aie::begin_restrict_vector<16>((bfloat16 *)output_vector);
41+
3142
AIE_PREPARE_FOR_PIPELINING
3243
AIE_LOOP_MIN_ITERATION_COUNT(64)
3344
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>();
45+
*it_out++ = gelu_tanh_approx_v16(*it_in++);
5646
}
57-
5847
event1();
48+
}
5949

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

6364
extern "C" {
@@ -67,4 +68,11 @@ void gelu_bf16(bfloat16 *restrict input, bfloat16 *restrict output, int input_si
6768
gelu_tanh_approx_bf16(input, output, input_size);
6869
}
6970

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

iron/operators/gemv/design.py

Lines changed: 20 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,20 @@ 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 (
96+
m_output % 16 == 0
97+
), f"gelu epilogue needs m_output % 16 == 0 (got {m_output})"
98+
gelu_kernel = Kernel(
99+
f"{func_prefix}gelu_tile_bf16",
100+
f"{func_prefix}{kernel_object}",
101+
[np.int32, L1_C_ty],
102+
)
88103

89104
A_L3L1_fifos = [
90105
ObjectFifo(L1_A_ty, name=f"A_L3L1_{i}", depth=2) for i in range(cols)
@@ -96,7 +111,7 @@ def my_matvec(
96111
ObjectFifo(L1_C_ty, name=f"C_L1L3_{i}", depth=2) for i in range(cols)
97112
]
98113

99-
def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec):
114+
def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec, gelu_kernel=None):
100115
one_idx = index.constant(1)
101116
for _ in range_(0xFFFFFFFF): # batch dim handled as part of this loop
102117
b = B_L3L1_fifo.acquire(1)
@@ -110,6 +125,8 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec):
110125
a = A_L3L1_fifo.acquire(1)
111126
matvec(m_input, output_row_offset, a, b, c)
112127
A_L3L1_fifo.release(1)
128+
if gelu_kernel is not None:
129+
gelu_kernel(m_output, c)
113130
C_L1L3_fifo.release(1)
114131
B_L3L1_fifo.release(1)
115132

@@ -121,7 +138,8 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec):
121138
B_L3L1_fifos[i].cons(),
122139
C_L1L3_fifos[i].prod(),
123140
matvec,
124-
],
141+
]
142+
+ ([gelu_kernel] if epilogue == "gelu" else []),
125143
)
126144
for i in range(cols)
127145
]

iron/operators/gemv/op.py

Lines changed: 53 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,25 @@ 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(
60+
f"unknown epilogue {self.epilogue!r} (expected 'none' or 'gelu')"
61+
)
62+
if self.epilogue == "gelu" and self.tile_size_output % 16 != 0:
63+
raise ValueError(
64+
f"gelu epilogue needs tile_size_output % 16 == 0 (got {self.tile_size_output})"
65+
)
5266

5367
MLIROperator.__init__(self, context=self.context)
5468

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

@@ -71,26 +93,46 @@ def get_mlir_artifact(self):
7193
),
7294
{
7395
"verbose": mlir_verbose,
74-
"kernel_object": f"gemv_{self.K}k_{self.kernel_vector_size}vs.o",
96+
"kernel_object": self._kernel_link_file,
97+
"epilogue": self.epilogue,
7598
},
7699
),
77100
)
78101

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

95137
def get_arg_spec(self):
96138
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
@@ -64,3 +64,13 @@ def generate_golden_reference_batched(M=128, K=128, num_batches=2, seed=42):
6464
for b in range(num_batches):
6565
C[b] = A[b] @ B[b]
6666
return {"A": A, "B": B, "C": C}
67+
68+
69+
def gelu_tanh_approx(x):
70+
"""Tanh-approximation GELU, matching aie_kernels/aie2p/gelu.cc.
71+
72+
0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3))). Computed in float32.
73+
"""
74+
xf = np.asarray(x, dtype=np.float32)
75+
inner = 0.79788456 * (xf + 0.044715 * xf**3)
76+
return 0.5 * xf * (1.0 + np.tanh(inner))

iron/operators/gemv/test.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
from iron.operators.gemv.reference import (
1010
generate_golden_reference,
1111
generate_golden_reference_batched,
12+
gelu_tanh_approx,
1213
)
14+
from iron.common.device_utils import get_kernel_dir
15+
import numpy as np
16+
import torch
1317
from iron.common.test_utils import run_test
1418

1519

@@ -131,3 +135,54 @@ def test_gemv_batched(
131135
print(f"Effective Bandwidth: {bandwidth_gbps:.6e} GB/s\n")
132136

133137
assert not errors, f"batched GEMV failed: {errors}"
138+
139+
140+
@pytest.mark.metrics(
141+
Latency=r"Latency \(us\): (?P<value>[\d\.]+)",
142+
Bandwidth=r"Effective Bandwidth: (?P<value>[\d\.e\+-]+) GB/s",
143+
Throughput=r"Throughput: (?P<value>[\d\.e\+-]+) GFLOP/s",
144+
)
145+
@pytest.mark.parametrize(
146+
"M,K,num_aie_columns,tile_size_input,tile_size_output",
147+
[
148+
pytest.param(128, 128, 1, 32, 128),
149+
pytest.param(2048, 8192, 1, 1, 2048),
150+
pytest.param(8192, 2048, 1, 4, 1024),
151+
],
152+
)
153+
def test_gemv_gelu(
154+
M, K, num_aie_columns, tile_size_input, tile_size_output, aie_context
155+
):
156+
"""GEMV with the fused GELU epilogue (NPU2-only) vs a gelu(A @ B) golden."""
157+
if get_kernel_dir() != "aie2p":
158+
pytest.skip("gemv gelu epilogue is only available on NPU2 (aie2p)")
159+
160+
golden_ref = generate_golden_reference(M=M, K=K)
161+
c_ref = golden_ref["C"].to(torch.float32).numpy()
162+
c_gelu = torch.from_numpy(gelu_tanh_approx(c_ref).astype(np.float32)).to(
163+
torch.bfloat16
164+
)
165+
166+
operator = GEMV(
167+
M=M,
168+
K=K,
169+
num_aie_columns=num_aie_columns,
170+
tile_size_input=tile_size_input,
171+
tile_size_output=tile_size_output,
172+
epilogue="gelu",
173+
context=aie_context,
174+
)
175+
176+
input_buffers = {"matrix": golden_ref["A"].flatten(), "vector": golden_ref["B"]}
177+
output_buffers = {"output": c_gelu}
178+
179+
errors, latency_us, bandwidth_gbps = run_test(
180+
operator, input_buffers, output_buffers, rel_tol=0.06, abs_tol=2e-2
181+
)
182+
183+
print(f"\nLatency: {latency_us:.1f} us")
184+
gflops = (2.0 * M * K) / (latency_us * 1e-6) / 1e9
185+
print(f"Throughput: {gflops:.6e} GFLOP/s")
186+
print(f"Effective Bandwidth: {bandwidth_gbps:.6e} GB/s\n")
187+
188+
assert not errors, f"Test failed with errors: {errors}"

0 commit comments

Comments
 (0)