载入中...
搜索中...
未找到
GpuBackend.cpp
浏览该文件的文档.
1#include "tensor/GpuBackend.h"
2#include "tensor/Graph.h"
3#include "tensor/KernelGen.h"
5#include "tensor/Optimizer.h"
6
8#include "gpgpu/Gpgpu.h"
9#include "gpgpu/GpuBuffer.h"
10#include "gpgpu/Sequence.h"
11
12#include "common/Exception.h"
13#include "common/config.h"
14
15#include <algorithm>
16#include <chrono>
17#include <cstdio>
18#include <cstring>
19#include <limits>
20#include <map>
21#include <memory>
22#include <vector>
23
24namespace eve::tensor {
25namespace {
26
27constexpr int kLocalSize = 256;
28constexpr int kAutotuneIters = 5;
29
30#ifdef EVENGINE_WEBGPU
31const char *kReduceSource = R"(
32@group(0) @binding(0) var<storage, read_write> input: array<f32>;
33@group(0) @binding(1) var<storage, read_write> partial: array<f32>;
34struct Params { data: array<vec4<f32>, 8>, }
35@group(0) @binding(8) var<uniform> pc: Params;
36var<workgroup> scratch: array<f32, 256>;
37@compute @workgroup_size(256)
38fn main(@builtin(local_invocation_id) lid: vec3<u32>,
39 @builtin(global_invocation_id) gid: vec3<u32>,
40 @builtin(workgroup_id) wid: vec3<u32>) {
41 let size = u32(pc.data[0].x + 0.5);
42 let op = u32(pc.data[0].y + 0.5);
43 scratch[lid.x] = select(select(-3.402823e38f, 3.402823e38f, op == 1u), 0.0f, op == 0u);
44 if (gid.x < size) { scratch[lid.x] = input[gid.x]; }
45 workgroupBarrier();
46 for (var step = 128u; step > 0u; step >>= 1u) {
47 if (lid.x < step) {
48 if (op == 0u) { scratch[lid.x] += scratch[lid.x + step]; }
49 else if (op == 1u) { scratch[lid.x] = min(scratch[lid.x], scratch[lid.x + step]); }
50 else { scratch[lid.x] = max(scratch[lid.x], scratch[lid.x + step]); }
51 }
52 workgroupBarrier();
53 }
54 if (lid.x == 0u) { partial[wid.x] = scratch[0]; }
55}
56)";
57#else
58const char *kReduceSource = R"(#version 450
59layout(local_size_x = 256) in;
60layout(set = 0, binding = 0) readonly buffer In { float a[]; };
61layout(set = 0, binding = 1) writeonly buffer Out { float partial[]; };
62layout(push_constant) uniform PC { float data[32]; } pc;
63shared float sdata[256];
64void main() {
65 uint tid = gl_LocalInvocationID.x;
66 uint gid = gl_GlobalInvocationID.x;
67 uint size = uint(pc.data[0] + 0.5);
68 int op = int(pc.data[1] + 0.5);
69 float ident = (op == 0) ? 0.0 : (op == 1 ? 3.402823e38 : -3.402823e38);
70 sdata[tid] = (gid < size) ? a[gid] : ident;
71 barrier();
72 for (uint s = 128u; s > 0u; s >>= 1u) {
73 if (tid < s) {
74 if (op == 0) sdata[tid] += sdata[tid + s];
75 else if (op == 1) sdata[tid] = min(sdata[tid], sdata[tid + s]);
76 else sdata[tid] = max(sdata[tid], sdata[tid + s]);
77 }
78 barrier();
79 }
80 if (tid == 0u) partial[gl_WorkGroupID.x] = sdata[0];
81}
82)";
83
84#endif
85
86struct ReduceKernels {
87 gpgpu::Gpgpu *gpgpu = nullptr;
88 gpgpu::ComputeShader *reduce = nullptr;
89};
90
92ReduceKernels *getReduceKernels() {
93 static ReduceKernels *kernels = nullptr;
94 static bool failed = false;
95 if (kernels) return kernels;
96 if (failed) return nullptr;
97 try {
98 auto *gp = gpgpu::Gpgpu::create();
99 if (!gp || !gp->isAvailable()) {
100 failed = true;
101 return nullptr;
102 }
103 auto *k = new ReduceKernels();
104 k->gpgpu = gp;
105 k->reduce = gp->newShader(kReduceSource);
106 kernels = k;
107 return kernels;
108 } catch (...) {
109 failed = true;
110 return nullptr;
111 }
112}
113
114int groupsFor(int count) { return (count + kLocalSize - 1) / kLocalSize; }
115
116Result<KernelSpec> backendKernel(const Graph &graph, const FusedGroup &group,
118#ifdef EVENGINE_WEBGPU
120#else
121 KernelSpec spec;
122 const bool generated = variant == KernelVariant::TiledMatMul ? generateMatMulVariant(graph, group, true, spec)
123 : generateKernel(graph, group, spec);
124 if (!generated)
126 Diagnostic::error(DiagnosticCode::Unsupported, "Tensor GLSL kernel generation failed", "tensor.kernel"));
127 return Result<KernelSpec>::success(std::move(spec));
128#endif
129}
130
132void bindKernel(gpgpu::ComputeShader *shader, const KernelSpec &spec,
133 const std::vector<gpgpu::GpuBuffer *> &inputs, gpgpu::GpuBuffer *output) {
134 for (int i = 0; i < spec.inputCount; ++i)
135 shader->bindBuffer(i,
136 spec.inputRepresentatives.empty() || spec.inputRepresentatives[static_cast<size_t>(i)] == i
137 ? inputs[static_cast<size_t>(i)]
138 : nullptr);
139 shader->bindBuffer(spec.inputCount, output);
140}
141
142double timeDispatch(gpgpu::Gpgpu *gp, gpgpu::ComputeShader *shader, int gx, int gy) {
143 const auto t0 = std::chrono::steady_clock::now();
144 for (int i = 0; i < kAutotuneIters; ++i) gp->dispatch(shader, gx, gy, 1);
145 const auto t1 = std::chrono::steady_clock::now();
146 return std::chrono::duration<double, std::milli>(t1 - t0).count() / kAutotuneIters;
147}
148
149} // namespace
150
154 std::unique_ptr<gpgpu::ComputeShader> pass1;
155 std::unique_ptr<gpgpu::ComputeShader> pass2;
156 std::vector<gpgpu::GpuBuffer *> inputs; // one per group input node
157 std::vector<gpgpu::GpuBuffer *> stats; // statsCount working buffers
158 gpgpu::GpuBuffer *qScales = nullptr; // per-group scales (int8/int4)
160 };
161
162 gpgpu::Gpgpu *gpgpu = nullptr;
163 std::vector<GroupRuntime> groups;
164 std::vector<gpgpu::GpuBuffer *> slotBuffer; // arena slot -> buffer
165 std::vector<gpgpu::GpuBuffer *> placeholderBuffers;
166 std::vector<int> placeholderSizes;
167 std::vector<std::unique_ptr<gpgpu::GpuBuffer>> ownedBuffers;
168 std::map<int, gpgpu::GpuBuffer *> qScalesByNode; // quantized const node -> scales
170 int outputSize = 0;
171 std::unique_ptr<gpgpu::Sequence> sequence;
172 std::unique_ptr<gpgpu::GpuBuffer> outputStaging;
173
175 // Retire command/descriptor references before their owning buffers.
176 sequence.reset();
177 groups.clear();
178 outputStaging.reset();
179 }
180
181 gpgpu::GpuBuffer *alloc(int byteSize) {
182 auto buffer = std::unique_ptr<gpgpu::GpuBuffer>(gpgpu->newBuffer(byteSize, "storage"));
183 auto* borrowed = buffer.get();
184 ownedBuffers.push_back(std::move(buffer));
185 return borrowed;
186 }
187
189 void bindGroup(const GroupRuntime &g) const {
190 if (g.spec.twoPass) {
191 auto *p1 = g.pass1.get();
192 for (int i = 0; i < g.spec.inputsReadPass1; ++i)
193 p1->bindBuffer(
194 i, g.spec.inputRepresentatives.empty() || g.spec.inputRepresentatives[static_cast<size_t>(i)] == i
195 ? g.inputs[static_cast<size_t>(i)]
196 : nullptr);
197 for (int s = 0; s < g.spec.statsCount; ++s)
198 p1->bindBuffer(g.spec.inputsReadPass1 + s, g.stats[static_cast<size_t>(s)]);
199 }
200 auto *p2 = g.pass2.get();
201 for (int i = 0; i < g.spec.inputCount; ++i)
202 p2->bindBuffer(
203 i, g.spec.inputRepresentatives.empty() || g.spec.inputRepresentatives[static_cast<size_t>(i)] == i
204 ? g.inputs[static_cast<size_t>(i)]
205 : nullptr);
206 if (g.spec.scalesBinding >= 0 && g.qScales)
207 p2->bindBuffer(g.spec.scalesBinding, g.qScales);
208 const int outBinding = g.spec.outputBinding >= 0 ? g.spec.outputBinding
209 : g.spec.inputCount;
210 if (g.spec.twoPass) {
211 for (int s = 0; s < g.spec.statsCount; ++s)
212 p2->bindBuffer(g.spec.inputCount + s, g.stats[static_cast<size_t>(s)]);
213 p2->bindBuffer(g.spec.inputCount + g.spec.statsCount, g.output);
214 } else {
215 p2->bindBuffer(outBinding, g.output);
216 }
217 }
218
220 void recordGroup(const GroupRuntime &g, gpgpu::Sequence *seq) const {
221 if (g.spec.twoPass)
222 seq->recordDispatch(g.pass1.get(), g.spec.groupsX1, g.spec.groupsY1,
223 g.spec.groupsZ1);
224 seq->recordDispatch(g.pass2.get(), g.spec.groupsX2, g.spec.groupsY2,
225 g.spec.groupsZ2);
226 }
227};
228
229GpuProgram::GpuProgram() : impl_(new Impl()) {}
230GpuProgram::~GpuProgram() { delete impl_; }
231
232GpuProgram *GpuProgram::tryBuild(const Graph &graph, const OptimizedGraph &opt, int outputNode) {
233 auto *gp = gpgpu::Gpgpu::create();
234 if (!gp || !gp->isAvailable()) return nullptr;
235 if (outputNode < 0 || outputNode >= graph.nodeCount()) return nullptr;
236
237 auto *prog = new GpuProgram();
238 prog->impl_->gpgpu = gp;
239 auto &impl = *prog->impl_;
240
241 try {
242 // arena buffers: one per memory-plan slot
243 impl.slotBuffer.resize(opt.slotSize.size(), nullptr);
244 for (size_t s = 0; s < opt.slotSize.size(); ++s)
245 impl.slotBuffer[s] = impl.alloc(opt.slotSize[s] * int(sizeof(float)));
246
247 impl.sequence.reset(gp->newSequence());
248
249 // placeholder / const uploads
250 impl.placeholderBuffers.clear();
251 impl.placeholderSizes.clear();
252 for (int id = 0; id < graph.nodeCount(); ++id) {
253 const auto &nd = graph.node(id);
254 if (opt.nodeSlot[static_cast<size_t>(id)] < 0) continue;
255 auto *buf = impl.slotBuffer[static_cast<size_t>(opt.nodeSlot[static_cast<size_t>(id)])];
256 if (nd.type == OpType::Placeholder) {
257 const int slot = nd.placeholderSlot;
258 if (slot < 0) throw eve::Exception("GpuProgram: bad placeholder slot");
259 if (int(impl.placeholderBuffers.size()) <= slot) {
260 impl.placeholderBuffers.resize(size_t(slot) + 1, nullptr);
261 impl.placeholderSizes.resize(size_t(slot) + 1, 0);
262 }
263 impl.placeholderBuffers[static_cast<size_t>(slot)] = buf;
264 impl.placeholderSizes[static_cast<size_t>(slot)] = nd.size;
265 } else if (nd.type == OpType::Const) {
266 if (!nd.constBytes.empty()) {
267 // Packed weights may end mid-word; WebGPU transfers require four-byte sizes.
268 if (nd.constBytes.size() % 4 != 0) {
269 auto bytes = nd.constBytes;
270 bytes.resize((bytes.size() + 3) & ~size_t(3), 0);
271 buf->uploadBytes(bytes.data(), bytes.size());
272 } else {
273 buf->uploadBytes(nd.constBytes.data(), nd.constBytes.size());
274 }
275 if (!nd.constScales.empty()) {
276 auto *sb = impl.alloc(int(nd.constScales.size()) * int(sizeof(float)));
277 sb->uploadBytes(nd.constScales.data(),
278 sizeof(float) * nd.constScales.size());
279 impl.qScalesByNode[id] = sb;
280 }
281 } else {
282 buf->uploadBytes(nd.constData.data(), sizeof(float) * size_t(nd.size));
283 }
284 }
285 }
286
287 // per-group kernels (topological execution order)
288 for (int gi : opt.groupOrder) {
289 const auto &grp = opt.groups[static_cast<size_t>(gi)];
290 if (grp.kind == GroupKind::Alias) continue; // pure buffer alias
291
293 for (int input : grp.inputs) {
294 const int slot = opt.nodeSlot[static_cast<size_t>(input)];
295 if (slot < 0) throw eve::Exception("GpuProgram: input without slot");
296 rt.inputs.push_back(impl.slotBuffer[static_cast<size_t>(slot)]);
297 const GraphNode &inN = graph.node(input);
298 if (!inN.constBytes.empty() && !inN.constScales.empty()) {
299 auto it = impl.qScalesByNode.find(input);
300 if (it != impl.qScalesByNode.end()) rt.qScales = it->second;
301 }
302 }
303 const int outSlot = opt.nodeSlot[static_cast<size_t>(grp.outputNode)];
304 if (outSlot < 0) throw eve::Exception("GpuProgram: output without slot");
305 rt.output = impl.slotBuffer[static_cast<size_t>(outSlot)];
306
307 KernelSpec spec;
308 if (grp.kind == GroupKind::MatMul) {
309 const GraphNode &mm = graph.node(grp.nodes.front());
310 auto naiveResult = backendKernel(graph, grp);
311 if (!naiveResult) throw eve::Exception("GpuProgram: matmul codegen failed");
312 KernelSpec naive = std::move(naiveResult).value();
313 if (mm.rank == 2) {
314 auto tiledResult = backendKernel(graph, grp, KernelVariant::TiledMatMul);
315 std::unique_ptr<gpgpu::ComputeShader> naiveShader, tiledShader;
316 if (tiledResult) {
317 KernelSpec tiled = std::move(tiledResult).value();
318 naiveShader.reset(gp->newShader(naive.pass2));
319 tiledShader.reset(gp->newShader(tiled.pass2));
320 try {
321 // Time with the real arena buffers bound: dispatching
322 // unbound kernels on the 4-byte dummy SSBO faults.
323 bindKernel(naiveShader.get(), naive, rt.inputs, rt.output);
324 const double tNaive =
325 timeDispatch(gp, naiveShader.get(), naive.groupsX2,
326 naive.groupsY2);
327 bindKernel(tiledShader.get(), tiled, rt.inputs, rt.output);
328 const double tTiled =
329 timeDispatch(gp, tiledShader.get(), tiled.groupsX2,
330 tiled.groupsY2);
331 if (tTiled < tNaive) {
332 spec = tiled;
333 rt.pass2 = std::move(tiledShader);
334 } else {
335 spec = naive;
336 rt.pass2 = std::move(naiveShader);
337 }
338 } catch (...) {
339 spec = naive;
340 rt.pass2 = std::move(naiveShader);
341 }
342 } else {
343 spec = naive;
344 rt.pass2.reset(gp->newShader(naive.pass2));
345 }
346 } else {
347 spec = naive;
348 rt.pass2.reset(gp->newShader(naive.pass2));
349 }
350 } else {
351 auto result = backendKernel(graph, grp);
352 if (!result) throw eve::Exception("GpuProgram: kernel codegen failed");
353 spec = std::move(result).value();
354 rt.pass2.reset(gp->newShader(spec.pass2));
355 if (spec.twoPass) rt.pass1.reset(gp->newShader(spec.pass1));
356 }
357 rt.spec = spec;
358
359 for (int s = 0; s < spec.statsCount; ++s)
360 rt.stats.push_back(impl.alloc(spec.statsSize * int(sizeof(float))));
361 impl.bindGroup(rt);
362 impl.groups.push_back(std::move(rt));
363 }
364
365 // final output
366 const int outSlot = opt.nodeSlot[static_cast<size_t>(outputNode)];
367 if (outSlot < 0) throw eve::Exception("GpuProgram: final output without slot");
368 impl.outputBuffer = impl.slotBuffer[static_cast<size_t>(outSlot)];
369 impl.outputSize = graph.node(outputNode).size;
370 impl.outputStaging.reset(
371 gp->newBuffer(impl.outputSize * int(sizeof(float)), "staging"));
372 return prog;
373 } catch (const std::exception &e) {
374 fprintf(stderr, "[tensor] GpuProgram::tryBuild failed: %s\n", e.what());
375 delete prog;
376 return nullptr;
377 } catch (...) {
378 delete prog;
379 return nullptr;
380 }
381}
382
383std::vector<float> GpuProgram::run(const std::vector<const float *> &feeds) const {
384 gpgpu::Sequence *seq = impl_->sequence.get();
385 seq->begin();
386 for (size_t i = 0; i < feeds.size(); ++i) {
387 if (i >= impl_->placeholderBuffers.size() || !impl_->placeholderBuffers[i]) continue;
388 seq->recordUpload(impl_->placeholderBuffers[i], feeds[i],
389 sizeof(float) * size_t(impl_->placeholderSizes[i]));
390 }
391 for (const auto &g : impl_->groups) impl_->recordGroup(g, seq);
392 const uint64_t outBytes = sizeof(float) * size_t(impl_->outputSize);
393 seq->recordDownload(impl_->outputBuffer, impl_->outputStaging.get(), outBytes);
394 seq->submit();
395
396 std::vector<float> out(static_cast<size_t>(impl_->outputSize));
397 impl_->outputStaging->downloadBytes(out.data(), outBytes);
398 return out;
399}
400
401bool gpuReduce(const float *data, int size, int op, float &outResult) {
402 if (!data || size <= 0) return false;
403 ReduceKernels *kernels = getReduceKernels();
404 if (!kernels) return false;
405 try {
406 std::unique_ptr<gpgpu::GpuBuffer> in(
407 kernels->gpgpu->newBuffer(size * int(sizeof(float)), "storage"));
408 in->uploadBytes(data, sizeof(float) * size_t(size));
409
410 const int groups = groupsFor(size);
411 std::unique_ptr<gpgpu::GpuBuffer> partial(
412 kernels->gpgpu->newBuffer(groups * int(sizeof(float)), "storage"));
413
414 kernels->reduce->bindBuffer(0, in.get());
415 kernels->reduce->bindBuffer(1, partial.get());
416 kernels->reduce->setFloat(0, float(size));
417 kernels->reduce->setFloat(1, float(op));
418 kernels->gpgpu->dispatch(kernels->reduce, groups);
419
420 std::vector<float> parts(static_cast<size_t>(groups));
421 partial->downloadBytes(parts.data(), sizeof(float) * size_t(groups));
422
423 float acc = op == 0 ? 0.f
424 : (op == 1 ? std::numeric_limits<float>::max()
425 : -std::numeric_limits<float>::max());
426 for (float v : parts) {
427 if (op == 0) acc += v;
428 else if (op == 1) acc = std::min(acc, v);
429 else acc = std::max(acc, v);
430 }
431 outResult = acc;
432 return true;
433 } catch (...) {
434 return false;
435 }
436}
437
438} // namespace eve::tensor
std::vector< eve::artifact::PartView > parts
std::string output
const std::string & s
std::string variant
building::EdgeCurveGroup group
void * impl
EvpackChunkInput input
Definition Evpack.cpp:170
tensor::Graph g
Definition GpuGraph.cpp:7
int inputs
Definition GridGraph.cpp:23
float v
std::uint64_t bytes
uint32_t groups
Definition OnnxGpgpu.cpp:39
gpgpu::Gpgpu & gp
Definition OnnxGpgpu.cpp:42
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
const RuntimeTensor * borrowed
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
std::string id
Definition PlayHost.cpp:108
Shader * shader
std::uint32_t count
float size
Definition TreeMesh.cpp:156
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
GPGPU module — compute shaders + storage buffers via the active Graphics backend. Uses the graphics q...
Definition Gpgpu.h:23
GpuBuffer * newBuffer(int byteSize, const std::string &usage="storage")
Allocate a GPU buffer. usage: "storage" (SSBO, device-local) | "staging" (host-visible transfer).
Definition Gpgpu.cpp:372
Backend-agnostic GPU buffer for compute (storage) or CPU staging transfers. Squirrel-owned; derived c...
Definition GpuBuffer.h:18
EVENGINE_API_WORLD public API.
Definition Sequence.h:41
void recordUpload(GpuBuffer *dst, const void *src, uint64_t nbytes, uint64_t dstOffset=0)
Record upload.
Definition Sequence.cpp:77
void recordDownload(GpuBuffer *src, GpuBuffer *staging, uint64_t nbytes, uint64_t srcOffset=0)
Record download.
Definition Sequence.cpp:82
void submit()
Submit.
Definition Sequence.cpp:92
void recordDispatch(ComputeShader *shader, int groupsX, int groupsY=1, int groupsZ=1)
Record dispatch.
Definition Sequence.cpp:87
void begin()
Begins begin.
Definition Sequence.cpp:75
GPU execution of a compiled tensor Graph via generated compute shaders.
Definition GpuBackend.h:28
std::vector< float > run(const std::vector< const float * > &feeds) const
feeds[slot] must point to placeholderSize(slot) floats. Returns the output buffer.
static GpuProgram * tryBuild(const Graph &graph, const OptimizedGraph &opt, int outputNode)
Try build.
~GpuProgram()
Gpu program.
EVENGINE_API_DOMAINS public API.
Definition Graph.h:111
Result< T > failed(Status status, eve::DiagnosticCode code, RuleId rule, std::string message)
Construct a failed editing result with explicit status and diagnostic categories.
int groupsFor(int count)
Groups for.
KernelVariant
Internal choice of generated matrix multiplication implementation.
bool generateKernel(const Graph &graph, const FusedGroup &group, KernelSpec &out)
Generate kernel.
bool generateMatMulVariant(const Graph &graph, const FusedGroup &group, bool tiled, KernelSpec &out)
Generate mat mul variant.
Result< KernelSpec > generateWgslKernel(const Graph &graph, const FusedGroup &group, KernelVariant variant)
Lower an optimizer-produced group directly to owning WGSL source and dispatch metadata.
bool gpuReduce(const float *data, int size, int op, float &outResult)
GPU-accelerated reduction for large eager tensors. op: 0 = sum, 1 = min, 2 = max. Returns false (call...
std::unique_ptr< gpgpu::ComputeShader > pass1
std::unique_ptr< gpgpu::ComputeShader > pass2
std::vector< gpgpu::GpuBuffer * > stats
std::vector< gpgpu::GpuBuffer * > inputs
std::vector< GroupRuntime > groups
std::vector< int > placeholderSizes
std::unique_ptr< gpgpu::Sequence > sequence
std::vector< gpgpu::GpuBuffer * > placeholderBuffers
gpgpu::GpuBuffer * alloc(int byteSize)
gpgpu::GpuBuffer * outputBuffer
void bindGroup(const GroupRuntime &g) const
std::vector< gpgpu::GpuBuffer * > slotBuffer
void recordGroup(const GroupRuntime &g, gpgpu::Sequence *seq) const
std::unique_ptr< gpgpu::GpuBuffer > outputStaging
std::map< int, gpgpu::GpuBuffer * > qScalesByNode
std::vector< std::unique_ptr< gpgpu::GpuBuffer > > ownedBuffers
GraphNode public API.
Definition Graph.h:80
std::vector< float > constScales
Definition Graph.h:106
std::vector< uint8_t > constBytes
Definition Graph.h:105
KernelSpec public API.
Definition KernelGen.h:27
OptimizedGraph public API.
Definition Optimizer.h:77
std::vector< int > slotSize
Definition Optimizer.h:87
std::vector< int > nodeSlot
Definition Optimizer.h:85
std::vector< int > groupOrder
Definition Optimizer.h:83
std::vector< FusedGroup > groups
Definition Optimizer.h:81
gpgpu::ComputeShader * reduce
gpgpu::Gpgpu * gpgpu