Skip to content

Commit 1a2c4da

Browse files
committed
[ExecuTorch][Vulkan] Add et_vk.fused_ce kernels (fused cross-entropy loss + dlogits)
Pull Request resolved: #20941 Vulkan GLSL kernels + handler for the training custom op `et_vk.fused_ce` (fused CE: per-row online-softmax loss + dlogits, then an N->1 loss sum). Per-row shared-memory reduction structured like `glsl/reduce_per_row_buffer.glsl` (`NWORKERS=64`, one workgroup/row, barrier tree-combine); two dispatch nodes share `loss_partial`, ordered by the runtime per-`vTensor` barrier (same pattern as `impl/SDPA.cpp` QK->softmax->AV). AOT registration already exists under the shared Vulkan partitioner. ghstack-source-id: 405059103 @exported-using-ghexport Differential Revision: [D111761780](https://our.internmc.facebook.com/intern/diff/D111761780/)
1 parent 5501136 commit 1a2c4da

5 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#version 450 core
10+
11+
${define_required_extensions(STORAGE, DTYPE)}
12+
13+
#define PRECISION ${PRECISION}
14+
#define T ${buffer_scalar_type(DTYPE)}
15+
16+
${define_active_storage_type(STORAGE)}
17+
18+
layout(std430) buffer;
19+
20+
${layout_declare_tensor(B, "w", "t_dlogits", DTYPE, "buffer")}
21+
${layout_declare_tensor(B, "w", "t_loss_partial", DTYPE, "buffer")}
22+
${layout_declare_tensor(B, "r", "t_logits", DTYPE, "buffer")}
23+
${layout_declare_tensor(B, "r", "t_labels", "int", "buffer")}
24+
25+
${layout_declare_ubo(B, "int", "vocab")}
26+
${layout_declare_ubo(B, "int", "n_rows")}
27+
${layout_declare_ubo(B, "float", "n_valid")}
28+
29+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
30+
31+
#define NWORKERS 64
32+
33+
shared float red_m[NWORKERS];
34+
shared float red_l[NWORKERS];
35+
36+
// Fused cross-entropy: one workgroup per row cooperatively reduces the vocab
37+
// dimension with a single-pass online softmax (running max + rescaled running
38+
// sum), then writes per-row loss and dlogits. Rows with label < 0 are masked.
39+
void main() {
40+
const uint row = gl_GlobalInvocationID.y;
41+
if (int(row) >= n_rows) {
42+
return;
43+
}
44+
45+
const uint tid = gl_LocalInvocationID.x;
46+
const uint V = uint(vocab);
47+
const uint base = row * V;
48+
const int lbl = t_labels[row];
49+
50+
if (lbl < 0) {
51+
for (uint j = tid; j < V; j += NWORKERS) {
52+
t_dlogits[base + j] = T(0);
53+
}
54+
if (tid == 0u) {
55+
t_loss_partial[row] = T(0);
56+
}
57+
return;
58+
}
59+
60+
// Single read pass: maintain a running max m and the sum l of exp(x - m),
61+
// rescaling l whenever a larger value updates m. Finite -3.4e38 init.
62+
float m = -3.4e38;
63+
float l = 0.0;
64+
for (uint j = tid; j < V; j += NWORKERS) {
65+
float x = float(t_logits[base + j]);
66+
if (x > m) {
67+
l = l * exp(m - x) + 1.0;
68+
m = x;
69+
} else {
70+
l = l + exp(x - m);
71+
}
72+
}
73+
red_m[tid] = m;
74+
red_l[tid] = l;
75+
memoryBarrierShared();
76+
barrier();
77+
78+
// Tree-combine the (m, l) pairs: m becomes the max, l is rescaled to it.
79+
for (uint s = NWORKERS / 2u; s > 0u; s >>= 1u) {
80+
if (tid < s) {
81+
float ma = red_m[tid];
82+
float la = red_l[tid];
83+
float mb = red_m[tid + s];
84+
float lb = red_l[tid + s];
85+
float mm = max(ma, mb);
86+
red_m[tid] = mm;
87+
red_l[tid] = la * exp(ma - mm) + lb * exp(mb - mm);
88+
}
89+
memoryBarrierShared();
90+
barrier();
91+
}
92+
93+
const float row_max = red_m[0];
94+
const float denom = red_l[0];
95+
const float inv = 1.0 / denom;
96+
const float scale = 1.0 / n_valid;
97+
98+
if (tid == 0u) {
99+
const float lse = row_max + log(denom);
100+
t_loss_partial[row] = T((lse - float(t_logits[base + uint(lbl)])) * scale);
101+
}
102+
103+
for (uint j = tid; j < V; j += NWORKERS) {
104+
float g = exp(float(t_logits[base + j]) - row_max) * inv * scale;
105+
if (j == uint(lbl)) {
106+
g = g - scale;
107+
}
108+
t_dlogits[base + j] = T(g);
109+
}
110+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
fused_ce:
8+
parameter_names_with_default_values:
9+
DTYPE: float
10+
STORAGE: buffer
11+
generate_variant_forall:
12+
DTYPE:
13+
- VALUE: float
14+
shader_variants:
15+
- NAME: fused_ce_buffer
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#version 450 core
10+
11+
${define_required_extensions(STORAGE, DTYPE)}
12+
13+
#define PRECISION ${PRECISION}
14+
#define T ${buffer_scalar_type(DTYPE)}
15+
16+
${define_active_storage_type(STORAGE)}
17+
18+
layout(std430) buffer;
19+
20+
${layout_declare_tensor(B, "w", "t_loss", DTYPE, "buffer")}
21+
${layout_declare_tensor(B, "r", "t_loss_partial", DTYPE, "buffer")}
22+
23+
${layout_declare_ubo(B, "int", "n_rows")}
24+
25+
layout(local_size_x_id = 0, local_size_y_id = 1, local_size_z_id = 2) in;
26+
27+
#define NWORKERS 64
28+
29+
shared float red[NWORKERS];
30+
31+
// Self-contained [N] -> [1] tree-sum of the per-row losses in one workgroup, so
32+
// fused_ce carries no cross-op reduce dependency.
33+
void main() {
34+
const uint tid = gl_LocalInvocationID.x;
35+
36+
float s = 0.0;
37+
for (uint j = tid; j < uint(n_rows); j += NWORKERS) {
38+
s += float(t_loss_partial[j]);
39+
}
40+
red[tid] = s;
41+
memoryBarrierShared();
42+
barrier();
43+
44+
for (uint k = NWORKERS / 2u; k > 0u; k >>= 1u) {
45+
if (tid < k) {
46+
red[tid] += red[tid + k];
47+
}
48+
memoryBarrierShared();
49+
barrier();
50+
}
51+
52+
if (tid == 0u) {
53+
t_loss[0] = T(red[0]);
54+
}
55+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
fused_ce_sum:
8+
parameter_names_with_default_values:
9+
DTYPE: float
10+
STORAGE: buffer
11+
generate_variant_forall:
12+
DTYPE:
13+
- VALUE: float
14+
shader_variants:
15+
- NAME: fused_ce_sum_buffer
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/vulkan/runtime/graph/ops/OperatorRegistry.h>
10+
11+
#include <executorch/backends/vulkan/runtime/graph/ops/DynamicDispatchNode.h>
12+
13+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Common.h>
14+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/Staging.h>
15+
16+
#include <executorch/backends/vulkan/runtime/graph/ops/impl/utils/TensorUtils.h>
17+
#include <executorch/backends/vulkan/runtime/graph/ops/utils/ShaderNameUtils.h>
18+
19+
namespace vkcompute {
20+
21+
using namespace utils;
22+
23+
utils::uvec3 fused_ce_global_wg_size(
24+
ComputeGraph* graph,
25+
const vkapi::ShaderInfo& shader,
26+
const std::vector<ArgGroup>& args,
27+
const std::vector<ValueRef>& resize_args) {
28+
(void)shader;
29+
(void)resize_args;
30+
const ValueRef loss_partial = args.at(0).refs.at(1);
31+
return {
32+
1u, utils::safe_downcast<uint32_t>(graph->numel_of(loss_partial)), 1u};
33+
}
34+
35+
utils::uvec3 fused_ce_local_wg_size(
36+
ComputeGraph* graph,
37+
const vkapi::ShaderInfo& shader,
38+
const utils::uvec3& global_workgroup_size,
39+
const std::vector<ArgGroup>& args,
40+
const std::vector<ValueRef>& resize_args) {
41+
(void)graph;
42+
(void)shader;
43+
(void)global_workgroup_size;
44+
(void)args;
45+
(void)resize_args;
46+
return {64u, 1u, 1u};
47+
}
48+
49+
utils::uvec3 fused_ce_sum_global_wg_size(
50+
ComputeGraph* graph,
51+
const vkapi::ShaderInfo& shader,
52+
const std::vector<ArgGroup>& args,
53+
const std::vector<ValueRef>& resize_args) {
54+
(void)graph;
55+
(void)shader;
56+
(void)args;
57+
(void)resize_args;
58+
return {1u, 1u, 1u};
59+
}
60+
61+
utils::uvec3 fused_ce_sum_local_wg_size(
62+
ComputeGraph* graph,
63+
const vkapi::ShaderInfo& shader,
64+
const utils::uvec3& global_workgroup_size,
65+
const std::vector<ArgGroup>& args,
66+
const std::vector<ValueRef>& resize_args) {
67+
(void)graph;
68+
(void)shader;
69+
(void)global_workgroup_size;
70+
(void)args;
71+
(void)resize_args;
72+
return {64u, 1u, 1u};
73+
}
74+
75+
void resize_fused_ce_node(
76+
ComputeGraph* graph,
77+
const std::vector<ArgGroup>& args,
78+
const std::vector<ValueRef>& resize_args) {
79+
(void)resize_args;
80+
const ValueRef dlogits = args.at(0).refs.at(0);
81+
const ValueRef loss_partial = args.at(0).refs.at(1);
82+
const ValueRef logits = args.at(1).refs.at(0);
83+
84+
const std::vector<int64_t> logits_sizes = graph->sizes_of(logits);
85+
graph->virtual_resize(dlogits, logits_sizes);
86+
graph->virtual_resize(loss_partial, {logits_sizes.at(0)});
87+
}
88+
89+
void resize_fused_ce_sum_node(
90+
ComputeGraph* graph,
91+
const std::vector<ArgGroup>& args,
92+
const std::vector<ValueRef>& resize_args) {
93+
(void)resize_args;
94+
const ValueRef loss = args.at(0).refs.at(0);
95+
// Rank-0 scalar, matching fused_ce_meta's logits.new_empty([]).
96+
graph->virtual_resize(loss, {});
97+
}
98+
99+
void fused_ce(ComputeGraph& graph, const std::vector<ValueRef>& args) {
100+
int arg_idx = 0;
101+
const ValueRef logits = args[arg_idx++];
102+
const ValueRef labels = args[arg_idx++];
103+
const ValueRef n_valid_ref = args[arg_idx++];
104+
const ValueRef out_tuple_ref = args[arg_idx];
105+
106+
// Read loss/dlogits via short-lived get_value_list() temporaries: no get_*()
107+
// pointer may be alive when loss_partial is added below, else adding Values
108+
// can reallocate values_ and check_no_active_value_ptrs() throws.
109+
const ValueRef loss = graph.get_value_list(out_tuple_ref)->at(0);
110+
const ValueRef dlogits = graph.get_value_list(out_tuple_ref)->at(1);
111+
112+
VK_CHECK_COND(
113+
graph.is_buffer_storage(logits) && graph.is_buffer_storage(dlogits),
114+
"fused_ce: logits and dlogits must use buffer storage");
115+
VK_CHECK_COND(
116+
graph.dim_of(logits) == 2, "fused_ce: logits must be 2D [n_rows, vocab]");
117+
VK_CHECK_COND(
118+
graph.sizes_of(dlogits) == graph.sizes_of(logits),
119+
"fused_ce: dlogits must match logits shape");
120+
VK_CHECK_COND(
121+
graph.dtype_of(labels) == vkapi::kInt, "fused_ce: labels must be int32");
122+
VK_CHECK_COND(graph.dim_of(labels) == 1, "fused_ce: labels must be 1D [N]");
123+
VK_CHECK_COND(
124+
graph.size_at<int64_t>(0, labels) == graph.size_at<int64_t>(0, logits),
125+
"fused_ce: labels length must equal number of rows");
126+
VK_CHECK_COND(graph.numel_of(loss) == 1, "fused_ce: loss must be a scalar");
127+
128+
const int32_t n_rows = graph.size_at<int32_t>(0, logits);
129+
const int32_t vocab = graph.size_at<int32_t>(1, logits);
130+
const float n_valid = graph.extract_scalar<float>(n_valid_ref);
131+
132+
// One workgroup per row; the Vulkan spec guarantees maxComputeWorkGroupCount
133+
// >= 65535 per dimension.
134+
VK_CHECK_COND(
135+
n_rows <= 65535, "fused_ce: n_rows exceeds max workgroup count");
136+
137+
// Per-row loss contributions, reduced to the scalar loss by the sum node.
138+
TmpTensor loss_partial(
139+
&graph, {n_rows}, graph.dtype_of(logits), utils::kBuffer);
140+
141+
std::string kernel_name = "fused_ce";
142+
kernel_name.reserve(kShaderNameReserve);
143+
add_storage_type_suffix(kernel_name, graph.storage_type_of(dlogits));
144+
add_dtype_suffix(kernel_name, graph.dtype_of(dlogits));
145+
146+
graph.execute_nodes().emplace_back(new DynamicDispatchNode(
147+
graph,
148+
VK_KERNEL_FROM_STR(kernel_name),
149+
fused_ce_global_wg_size,
150+
fused_ce_local_wg_size,
151+
// Inputs and Outputs
152+
{{{dlogits, loss_partial}, vkapi::kWrite},
153+
{{logits, labels}, vkapi::kRead}},
154+
// Shader params buffers
155+
{graph.create_params_buffer(vocab),
156+
graph.create_params_buffer(n_rows),
157+
graph.create_params_buffer(n_valid)},
158+
// Push Constants
159+
{},
160+
// Specialization Constants
161+
{},
162+
// Resize Args
163+
{},
164+
// Resizing Logic
165+
resize_fused_ce_node));
166+
167+
std::string sum_kernel_name = "fused_ce_sum";
168+
sum_kernel_name.reserve(kShaderNameReserve);
169+
add_storage_type_suffix(sum_kernel_name, graph.storage_type_of(loss));
170+
add_dtype_suffix(sum_kernel_name, graph.dtype_of(loss));
171+
172+
// Separate dispatch node so the loss_partial write (above) is ordered before
173+
// this read: the graph inserts a per-tensor pipeline barrier between them.
174+
graph.execute_nodes().emplace_back(new DynamicDispatchNode(
175+
graph,
176+
VK_KERNEL_FROM_STR(sum_kernel_name),
177+
fused_ce_sum_global_wg_size,
178+
fused_ce_sum_local_wg_size,
179+
// Inputs and Outputs
180+
{{loss, vkapi::kWrite}, {loss_partial, vkapi::kRead}},
181+
// Shader params buffers
182+
{graph.create_params_buffer(n_rows)},
183+
// Push Constants
184+
{},
185+
// Specialization Constants
186+
{},
187+
// Resize Args
188+
{},
189+
// Resizing Logic
190+
resize_fused_ce_sum_node));
191+
}
192+
193+
REGISTER_OPERATORS {
194+
VK_REGISTER_OP(et_vk.fused_ce.default, fused_ce);
195+
}
196+
197+
} // namespace vkcompute

0 commit comments

Comments
 (0)