1use super::*;
5use rlx_ir::OpKind;
6use rlx_wgpu::backend::WgpuExecutable;
7
8pub struct WgpuBackend;
9
10impl Backend for WgpuBackend {
11 fn supported_ops(&self) -> &'static [OpKind] {
12 rlx_wgpu::SUPPORTED_OPS
13 }
14
15 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
16 use rlx_opt::pass::Pass as _;
17 let graph = rlx_opt::LowerControlFlow.run(graph);
18 let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, rlx_wgpu::SUPPORTED_OPS)
19 .unwrap_or_else(|errors| {
20 panic!("{}", rlx_opt::format_legalize_error("wgpu", &errors));
21 });
22 let graph = apply_scan_device_preference(graph, options);
23 let graph = crate::precompile::precompile_cleanup(graph, options);
24 let graph = rlx_opt::LegalizeBroadcast.run(graph);
28 let compile_result = crate::stages::compile_graph_stages_for_backend(
37 rlx_driver::Device::Gpu,
38 graph,
39 options,
40 rlx_wgpu::SUPPORTED_OPS,
41 );
42 crate::stages::maybe_log_fusion(&compile_result.fusion);
43 let graph = compile_result.lir.into_graph();
44 let graph = match options.policy.clone() {
45 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
46 None => graph,
47 };
48 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
49 Box::new(WgpuExecutableWrapper {
50 inner: WgpuExecutable::compile_rng(graph, options.rng),
51 io_manifest,
52 })
53 }
54
55 fn compile_lir(&self, lir: LirModule, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
56 use rlx_opt::pass::Pass as _;
57 let graph = rlx_opt::LegalizeBroadcast.run(lir.into_graph());
60 let graph = prepare_fused_graph(graph, options, rlx_wgpu::SUPPORTED_OPS, "wgpu");
61 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
62 Box::new(WgpuExecutableWrapper {
63 inner: WgpuExecutable::compile_rng(graph, options.rng),
64 io_manifest,
65 })
66 }
67}
68
69struct WgpuExecutableWrapper {
70 inner: WgpuExecutable,
71 io_manifest: cpu_low_precision::IoDtypeManifest,
72}
73
74unsafe impl Send for WgpuExecutableWrapper {}
75
76impl ExecutableGraph for WgpuExecutableWrapper {
77 fn capabilities(&self) -> crate::ExecutableCapabilities {
78 crate::ExecutableCapabilities {
79 clone: true,
80 gpu_handles: true,
81 kv_resident: true,
86 typed_io: true,
87 active_extent: true,
88 ..crate::ExecutableCapabilities::NONE
89 }
90 }
91
92 fn set_param(&mut self, name: &str, data: &[f32]) {
93 self.inner.set_param(name, data);
94 }
95 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
96 self.inner.run(inputs)
97 }
98 fn run_read_outputs(
99 &mut self,
100 inputs: &[(&str, &[f32])],
101 read_indices: Option<&[usize]>,
102 ) -> Vec<Vec<f32>> {
103 self.inner.run_read_outputs(inputs, read_indices)
104 }
105 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
106 self.inner.bind_gpu_handle(name, data)
107 }
108 fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
109 self.inner.register_kv_row_feed(handle_name, output_index);
110 true
111 }
112 fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
113 self.inner.feed_kv_row(src_row, dst_row, row_elems)
114 }
115 fn has_gpu_handle(&self, name: &str) -> bool {
116 self.inner.has_gpu_handle(name)
117 }
118 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
119 self.inner.set_gpu_handle_feed(handle_name, output_index);
120 true
121 }
122 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
123 self.inner.read_gpu_handle(name)
124 }
125 fn prepare_resident_gpu_handle(&mut self, name: &str) -> bool {
126 self.inner.prepare_resident_gpu_handle(name)
127 }
128 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
129 self.inner.set_active_extent(extent);
130 }
131
132 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
133 self.inner.set_rng(rng);
134 }
135
136 fn rng(&self) -> rlx_ir::RngOptions {
137 self.inner.rng()
138 }
139
140 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
145 match dtype {
146 rlx_ir::DType::U8 | rlx_ir::DType::I8 => {
147 self.inner.set_param_bytes(name, data);
148 }
149 rlx_ir::DType::F32 => {
150 let n = data.len() / 4;
151 let f32_slice =
152 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
153 self.inner.set_param(name, f32_slice);
154 }
155 rlx_ir::DType::BF16 => {
156 if !self.inner.set_param_bf16_packed(name, data) {
160 self.inner.set_param_bytes(name, data);
171 }
172 }
173 rlx_ir::DType::F16
174 | rlx_ir::DType::F64
175 | rlx_ir::DType::I64
176 | rlx_ir::DType::I32
177 | rlx_ir::DType::U32
178 | rlx_ir::DType::Bool => {
179 let f32 = super::widen_bytes_to_f32(data, dtype);
180 self.inner.set_param(name, &f32);
181 }
182 other => panic!(
183 "rlx-wgpu set_param_typed: dtype {other:?} unsupported \
184 (f32-uniform arena; widen small ints/floats to F32)"
185 ),
186 }
187 }
188
189 fn run_typed(
192 &mut self,
193 inputs: &[(&str, &[u8], rlx_ir::DType)],
194 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
195 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
196 for (name, data, dt) in inputs {
197 let v: Vec<f32> = match *dt {
198 rlx_ir::DType::F32 => {
199 let n = data.len() / 4;
200 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec()
201 }
202 rlx_ir::DType::F16 => {
203 let n = data.len() / 2;
204 let s =
205 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::f16, n) };
206 s.iter().map(|h| h.to_f32()).collect()
207 }
208 rlx_ir::DType::BF16 => {
209 let n = data.len() / 2;
210 let s = unsafe {
211 std::slice::from_raw_parts(data.as_ptr() as *const half::bf16, n)
212 };
213 s.iter().map(|h| h.to_f32()).collect()
214 }
215 rlx_ir::DType::I64 => {
219 let n = data.len() / 8;
220 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i64, n) };
221 s.iter().map(|&x| x as f32).collect()
222 }
223 rlx_ir::DType::I32 => {
224 let n = data.len() / 4;
225 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i32, n) };
226 s.iter().map(|&x| x as f32).collect()
227 }
228 rlx_ir::DType::U8 | rlx_ir::DType::I8 | rlx_ir::DType::Bool => {
229 data.iter().map(|&b| b as f32).collect()
230 }
231 other => {
232 panic!("rlx-wgpu run_typed: input '{name}' dtype {other:?} unsupported")
233 }
234 };
235 owned.push((name.to_string(), v));
236 }
237 let refs: Vec<(&str, &[f32])> = owned
238 .iter()
239 .map(|(n, d)| (n.as_str(), d.as_slice()))
240 .collect();
241 let dtypes = super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
242 let outs = self.inner.run(&refs);
243 outs.into_iter()
244 .zip(
245 dtypes
246 .into_iter()
247 .chain(std::iter::repeat(rlx_ir::DType::F32)),
248 )
249 .map(|(v, dt)| (narrow_to_dtype(&v, dt), dt))
250 .collect()
251 }
252
253 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
254 Box::new(WgpuExecutableWrapper {
255 inner: self.inner.clone_for_cache(),
256 io_manifest: self.io_manifest.clone(),
257 })
258 }
259
260 #[cfg(target_arch = "wasm32")]
261 fn wgpu_requires_host(&self) -> bool {
262 self.inner.requires_host_execution()
263 }
264
265 #[cfg(target_arch = "wasm32")]
266 fn wgpu_run_async<'a>(
267 &'a mut self,
268 inputs: &'a [(&'a str, &'a [f32])],
269 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<Vec<f32>>, String>> + 'a>>
270 {
271 if self.inner.requires_host_execution() {
272 return Box::pin(async move {
273 Err(
274 "WebGPU wasm graph contains host-executed ops (GGUF dequant, LSTM, \
275 collectives, FFT-host, …) — use WebGL fallback or CPU"
276 .into(),
277 )
278 });
279 }
280 Box::pin(async move { Ok(self.inner.run_async(inputs).await) })
281 }
282}
283
284fn narrow_to_dtype(v: &[f32], dt: rlx_ir::DType) -> Vec<u8> {
290 use rlx_ir::DType;
291 match dt {
292 DType::F32 => {
293 let mut bytes = Vec::with_capacity(v.len() * 4);
294 for &x in v {
295 bytes.extend_from_slice(&x.to_le_bytes());
296 }
297 bytes
298 }
299 DType::F16 => {
300 let mut bytes = Vec::with_capacity(v.len() * 2);
301 for &x in v {
302 bytes.extend_from_slice(&half::f16::from_f32(x).to_le_bytes());
303 }
304 bytes
305 }
306 DType::BF16 => {
307 let mut bytes = Vec::with_capacity(v.len() * 2);
308 for &x in v {
309 bytes.extend_from_slice(&half::bf16::from_f32(x).to_le_bytes());
310 }
311 bytes
312 }
313 DType::F64 => {
314 let mut bytes = Vec::with_capacity(v.len() * 8);
315 for &x in v {
316 bytes.extend_from_slice(&(x as f64).to_le_bytes());
317 }
318 bytes
319 }
320 DType::I8 => v.iter().map(|&x| x as i8 as u8).collect(),
321 DType::U8 => v.iter().map(|&x| x as u8).collect(),
322 DType::I16 => {
323 let mut bytes = Vec::with_capacity(v.len() * 2);
324 for &x in v {
325 bytes.extend_from_slice(&(x as i16).to_le_bytes());
326 }
327 bytes
328 }
329 DType::I32 => {
330 let mut bytes = Vec::with_capacity(v.len() * 4);
331 for &x in v {
332 bytes.extend_from_slice(&(x as i32).to_le_bytes());
333 }
334 bytes
335 }
336 DType::U32 => {
337 let mut bytes = Vec::with_capacity(v.len() * 4);
338 for &x in v {
339 bytes.extend_from_slice(&(x as u32).to_le_bytes());
340 }
341 bytes
342 }
343 DType::I64 => {
344 let mut bytes = Vec::with_capacity(v.len() * 8);
345 for &x in v {
346 bytes.extend_from_slice(&(x as i64).to_le_bytes());
347 }
348 bytes
349 }
350 DType::Bool => v
351 .iter()
352 .map(|&x| if x != 0.0 { 1u8 } else { 0u8 })
353 .collect(),
354 DType::C64 => {
361 let mut bytes = Vec::with_capacity(v.len() * 4);
362 for &x in v {
363 bytes.extend_from_slice(&x.to_le_bytes());
364 }
365 bytes
366 }
367 DType::C128 => {
372 let mut bytes = Vec::with_capacity(v.len() * 8);
373 for &x in v {
374 bytes.extend_from_slice(&(x as f64).to_le_bytes());
375 }
376 bytes
377 }
378 }
379}