Skip to main content

cubek_reduce/launch/
base.rs

1use crate::{
2    ReduceError, ReducePrecision, VectorizationMode,
3    components::{
4        args::{NumericVector, ReduceArgs, TensorArgs, init_tensors},
5        global::{
6            cube::GlobalFullCubeReduce, plane::GlobalFullPlaneReduce, unit::GlobalFullUnitReduce,
7        },
8        instructions::*,
9    },
10    launch::{ReduceStrategy, RoutineStrategy, generate_vector_size},
11    output_vectorization_axis,
12    routines::{
13        GlobalReduceBlueprint, ReduceBlueprint, ReduceLaunchSettings, ReduceProblem,
14        ReduceVectorSettings, Routine, cube::CubeRoutine, plane::PlaneRoutine, unit::UnitRoutine,
15    },
16};
17use cubecl::{prelude::*, std::tensor::r#virtual::VirtualTensor};
18
19/// How many candidate slots a top-k thread may keep across its vector components
20/// before the reduce reads its input scalar instead: `k * vector_size` values
21/// and as many coordinates live per thread.
22const TOPK_VECTOR_SLOTS: usize = 32;
23
24#[derive(Clone, Copy, Debug)]
25pub struct ReduceDtypes {
26    pub input: ElemType,
27    pub output: ElemType,
28    pub accumulation: ElemType,
29}
30
31/// Dtypes for a reduce that writes its values and their indices at once.
32#[derive(Clone, Copy, Debug)]
33pub struct ReduceWithIndicesDtypes {
34    pub input: ElemType,
35    /// Dtype of the values output.
36    pub values: ElemType,
37    /// Dtype of the indices output.
38    pub indices: ElemType,
39    pub accumulation: ElemType,
40}
41
42impl ReduceWithIndicesDtypes {
43    fn values_dtypes(&self) -> ReduceDtypes {
44        ReduceDtypes {
45            input: self.input,
46            output: self.values,
47            accumulation: self.accumulation,
48        }
49    }
50}
51
52/// Analyse the problem and prepare the routine for launch: everything both entrypoints
53/// share between validation and the actual kernel launch. `output` is the tensor whose
54/// layout drives vectorization (the values tensor on the fused path).
55///
56/// `second_output` is the fused path's index tensor dtype: it shares the values tensor's
57/// layout but not its dtype, so the chosen output width must be legal for both. Passing it
58/// caps the width to one the second dtype also supports, rather than dropping to scalar on
59/// any width mismatch. `None` for the single-output path.
60///
61/// Returns the blueprint, the launch settings, and the output vectorization axis.
62#[allow(clippy::too_many_arguments)]
63fn prepare_reduce_launch(
64    client: &Client,
65    input: &TensorBinding,
66    output: &TensorBinding,
67    reduce_axis: usize,
68    strategy: ReduceStrategy,
69    dtypes: ReduceDtypes,
70    inst: ReduceOperationConfig,
71    address_type: AddressType,
72    second_output: Option<ElemType>,
73) -> Result<(ReduceBlueprint, ReduceLaunchSettings, usize), ReduceError> {
74    // Number of distinct reductions = product of non-reduce input dims. Taken as that
75    // product rather than `input_elems / reduce_len`, which divides by zero on a reduce
76    // axis of length zero. `validate_shapes` rejects such an axis before any caller
77    // reaches here, so this only keeps the arithmetic sound on its own.
78    let reduce_len = input.shape[reduce_axis];
79    let reduce_count: usize = input
80        .shape
81        .iter()
82        .enumerate()
83        .filter(|(axis, _)| *axis != reduce_axis)
84        .map(|(_, len)| len)
85        .product();
86
87    let problem = ReduceProblem {
88        reduce_len,
89        reduce_count,
90        axis: reduce_axis,
91        dtypes,
92        instruction: inst,
93        address_type,
94    };
95    let vectorization_mode = match input.strides[reduce_axis] {
96        1 => VectorizationMode::Parallel,
97        _ => VectorizationMode::Perpendicular,
98    };
99
100    let out_vec_axis = output_vectorization_axis(&input.strides, reduce_axis, vectorization_mode);
101
102    let (vector_size_input, vector_size_output) = generate_vector_size(
103        client,
104        input,
105        output,
106        reduce_axis,
107        problem.dtypes.input,
108        vectorization_mode,
109        &strategy.vectorization,
110    );
111    // The rolled top-k selection network (`k * k > TOPK_UNROLL_BUDGET`) keeps
112    // per-component accumulator and finalize arrays whose dynamic indexing places
113    // them in per-thread local memory, and their footprint scales with
114    // `k * vector_size` (about 48 bytes per slot at width 8). Past roughly
115    // 4 KiB per thread the NVIDIA Vulkan driver corrupts memory around the
116    // local-memory window (observed on 610.62 with an RTX 5090, with both the
117    // SPIR-V and the WGSL compiler: k = 64 at width 8 is clean, k = 96 faults):
118    // the kernel still returns correct results, but a later dispatch dies with
119    // `VK_ERROR_DEVICE_LOST`. Keep the rolled path scalar - its cost is
120    // dominated by the k-slot selection network, not the input read - so the
121    // slots shrink about six-fold, which holds k = 300, the object-detection
122    // case that exposed this, comfortably below the fault threshold.
123    //
124    // Past a handful of slots the vector costs even where the rolled path is not
125    // taken: every component of it keeps its own `k` candidates, values and
126    // coordinates, so width 8 at `k = 20` is 320 accumulator registers a thread.
127    // The kernel spills and the cube shrinks to a single plane; scalar, the
128    // same top-20 of a 151936-wide row (a vocabulary, the sampler's case) ran
129    // 2.4x faster on GP100 (4.4 ms -> 1.8 ms).
130    let (vector_size_input, vector_size_output) = match &problem.instruction {
131        ReduceOperationConfig::TopK(k) | ReduceOperationConfig::ArgTopK(k)
132            if *k * vector_size_input > TOPK_VECTOR_SLOTS
133                || *k * *k > crate::components::instructions::TOPK_UNROLL_BUDGET =>
134        {
135            (1, 1)
136        }
137        _ => (vector_size_input, vector_size_output),
138    };
139
140    // Both fused outputs share this width, so it must be legal for the index dtype too.
141    // Cap to the largest width the index dtype supports that does not exceed the values
142    // width (widths are powers of two, so the cap still divides the layout constraints the
143    // values width already satisfied). Only drops to scalar if the index dtype truly cannot
144    // vectorize; the common case (equal-width dtypes) is unchanged.
145    let vector_size_output = match second_output {
146        None => vector_size_output,
147        Some(index_dtype) => client
148            .io_optimized_vector_sizes(index_dtype.size())
149            .filter(|&width| width <= vector_size_output)
150            .max()
151            .unwrap_or(1),
152    };
153    let settings = ReduceVectorSettings {
154        vectorization_mode,
155        vector_size_input,
156        vector_size_output,
157        unchecked_fast_paths: matches!(
158            strategy.autotune_level,
159            cubecl::config::autotune::AutotuneLevel::Full
160        ),
161        fuse_on_read: false,
162    };
163
164    let (blueprint, settings) = match strategy.routine {
165        RoutineStrategy::Unit(strategy) => {
166            let routine = UnitRoutine;
167            routine.prepare(client, problem, settings, strategy)?
168        }
169        RoutineStrategy::Plane(strategy) => {
170            let routine = PlaneRoutine;
171            routine.prepare(client, problem, settings, strategy)?
172        }
173        RoutineStrategy::Cube(strategy) => {
174            let routine = CubeRoutine;
175            routine.prepare(client, problem, settings, strategy)?
176        }
177    };
178
179    Ok((blueprint, settings, out_vec_axis))
180}
181
182/// Launch a reduce kernel. This function assumes that all parameters are already validated.
183/// See the main entrypoint `reduce` in `lib.rs` for an example how to call this function
184/// with the appropriate assumptions.
185#[allow(clippy::too_many_arguments)]
186pub(crate) fn launch_reduce(
187    client: &Client,
188    input: TensorBinding,
189    output: TensorBinding,
190    reduce_axis: usize,
191    strategy: ReduceStrategy,
192    dtypes: ReduceDtypes,
193    inst: ReduceOperationConfig,
194) -> Result<(), ReduceError> {
195    let address_type = input
196        .required_address_type(dtypes.input.size())
197        .max(output.required_address_type(dtypes.output.size()));
198
199    let (blueprint, settings, out_vec_axis) = prepare_reduce_launch(
200        client,
201        &input,
202        &output,
203        reduce_axis,
204        strategy,
205        dtypes,
206        inst,
207        address_type,
208        None,
209    )?;
210
211    unsafe {
212        reduce_kernel::launch_unchecked::<TensorArgs>(
213            client,
214            settings.cube_count,
215            settings.cube_dim,
216            settings.address_type,
217            settings.vector.vector_size_input,
218            settings.vector.vector_size_output,
219            input.into_tensor_arg(),
220            output.into_tensor_arg(),
221            reduce_axis,
222            out_vec_axis,
223            blueprint,
224            inst,
225            dtypes.input,
226            dtypes.output,
227            dtypes.accumulation,
228        )
229    };
230
231    Ok(())
232}
233
234#[cube(launch_unchecked, address_type = "dynamic")]
235pub fn reduce_kernel<
236    In: Numeric,
237    InSize: Size,
238    Out: Numeric,
239    OutSize: Size,
240    Acc: Numeric,
241    RA: ReduceArgs,
242>(
243    input: &RA::Input<In, InSize>,
244    output: &mut RA::Output<Out, OutSize>,
245    reduce_axis: usize,
246    out_vec_axis: usize,
247    #[comptime] blueprint: ReduceBlueprint,
248    #[comptime] config: ReduceOperationConfig,
249    #[define(In)] _input_dtype: ElemType,
250    #[define(Out)] _output_dtype: ElemType,
251    #[define(Acc)] _acc_dtype: ElemType,
252) {
253    let (input, mut output) = init_tensors::<RA, In, InSize, Out, OutSize>(input, output);
254    reduce_kernel_virtual::<In, InSize, Out, OutSize, Acc>(
255        &input,
256        &mut output,
257        reduce_axis,
258        out_vec_axis,
259        blueprint,
260        config,
261    );
262}
263
264/// Launch a reduce kernel writing both the values and their indices. This function assumes
265/// that all parameters are already validated; see `reduce_with_indices` in `lib.rs`.
266///
267/// Both halves are written, so the instruction always tracks coordinates regardless
268/// of which config of a values/`Arg*` pair the caller reached this path with. The
269/// fused `to_output_both_*` conversions ignore the mode; it only sizes the
270/// accumulator, and `Indices` is what turns coordinate tracking on.
271#[allow(clippy::too_many_arguments)]
272pub(crate) fn launch_reduce_with_indices(
273    client: &Client,
274    input: TensorBinding,
275    values: TensorBinding,
276    indices: TensorBinding,
277    reduce_axis: usize,
278    strategy: ReduceStrategy,
279    dtypes: ReduceWithIndicesDtypes,
280    operation: ReduceOperationConfig,
281) -> Result<(), ReduceError> {
282    match operation {
283        ReduceOperationConfig::TopK(k) | ReduceOperationConfig::ArgTopK(k) => launch_fused::<TopK>(
284            client,
285            input,
286            values,
287            indices,
288            reduce_axis,
289            strategy,
290            dtypes,
291            TopKConfig {
292                k,
293                output: ReduceOutputMode::Indices,
294            },
295            ReduceOperationConfig::ArgTopK(k),
296        ),
297        ReduceOperationConfig::Max | ReduceOperationConfig::ArgMax => launch_fused::<Max>(
298            client,
299            input,
300            values,
301            indices,
302            reduce_axis,
303            strategy,
304            dtypes,
305            ReduceOutputMode::Indices,
306            ReduceOperationConfig::ArgMax,
307        ),
308        ReduceOperationConfig::Min | ReduceOperationConfig::ArgMin => launch_fused::<Min>(
309            client,
310            input,
311            values,
312            indices,
313            reduce_axis,
314            strategy,
315            dtypes,
316            ReduceOutputMode::Indices,
317            ReduceOperationConfig::ArgMin,
318        ),
319        _ => unreachable!("reduce_with_indices rejects operations without indices"),
320    }
321}
322
323/// The launch shared by every fused operation: only the instruction family, its
324/// config, and the `Arg*` config sizing the blueprint differ.
325#[allow(clippy::too_many_arguments)]
326fn launch_fused<R: ReduceWithIndicesFamily>(
327    client: &Client,
328    input: TensorBinding,
329    values: TensorBinding,
330    indices: TensorBinding,
331    reduce_axis: usize,
332    strategy: ReduceStrategy,
333    dtypes: ReduceWithIndicesDtypes,
334    config: R::Config,
335    blueprint_operation: ReduceOperationConfig,
336) -> Result<(), ReduceError> {
337    let address_type = input
338        .required_address_type(dtypes.input.size())
339        .max(values.required_address_type(dtypes.values.size()))
340        .max(indices.required_address_type(dtypes.indices.size()));
341
342    let (blueprint, settings, out_vec_axis) = prepare_reduce_launch(
343        client,
344        &input,
345        &values,
346        reduce_axis,
347        strategy,
348        dtypes.values_dtypes(),
349        // Always size the blueprint as the Arg* config, never the values one:
350        // this path tracks coordinates whichever config the caller passed, so
351        // the shared accumulator needs its index slices too. Sizing it as the
352        // values config would under-allocate shared memory.
353        blueprint_operation,
354        address_type,
355        // The index output shares the values layout but not its dtype, so the
356        // shared output width must stay legal for the index dtype too.
357        Some(dtypes.indices),
358    )?;
359
360    unsafe {
361        reduce_with_indices_kernel::launch_unchecked::<TensorArgs, R>(
362            client,
363            settings.cube_count,
364            settings.cube_dim,
365            settings.address_type,
366            settings.vector.vector_size_input,
367            settings.vector.vector_size_output,
368            settings.vector.vector_size_output,
369            input.into_tensor_arg(),
370            values.into_tensor_arg(),
371            indices.into_tensor_arg(),
372            reduce_axis,
373            out_vec_axis,
374            blueprint,
375            config,
376            dtypes.input,
377            dtypes.values,
378            dtypes.indices,
379            dtypes.accumulation,
380        )
381    };
382
383    Ok(())
384}
385
386/// Reduce `input` along `reduce_axis`, writing both the values and their indices.
387///
388/// The indices output goes through [`ReduceArgs`] like the value output, so both
389/// can be virtualized the same way; today only [`TensorArgs`] is used here.
390#[cube(launch_unchecked, address_type = "dynamic")]
391pub fn reduce_with_indices_kernel<
392    In: Numeric,
393    InSize: Size,
394    Out: Numeric,
395    OutSize: Size,
396    Idx: Numeric,
397    IdxSize: Size,
398    Acc: Numeric,
399    RA: ReduceArgs,
400    R: ReduceWithIndicesFamily,
401>(
402    input: &RA::Input<In, InSize>,
403    output: &mut RA::Output<Out, OutSize>,
404    indices: &mut RA::Output<Idx, IdxSize>,
405    reduce_axis: usize,
406    out_vec_axis: usize,
407    #[comptime] blueprint: ReduceBlueprint,
408    #[comptime] config: R::Config,
409    #[define(In)] _input_dtype: ElemType,
410    #[define(Out)] _output_dtype: ElemType,
411    #[define(Idx)] _indices_dtype: ElemType,
412    #[define(Acc)] _acc_dtype: ElemType,
413) {
414    let (input_values, mut output) = init_tensors::<RA, In, InSize, Out, OutSize>(input, output);
415    // Pairs the same input with the index output to build its virtual tensor;
416    // the duplicate input tensor is comptime plumbing with no runtime cost.
417    let (_input_indices, mut indices) =
418        init_tensors::<RA, In, InSize, Idx, IdxSize>(input, indices);
419
420    reduce_with_indices_kernel_inner::<(In, InSize, Acc), (Out, OutSize), (Idx, IdxSize), R>(
421        &input_values,
422        &mut output,
423        &mut indices,
424        reduce_axis,
425        out_vec_axis,
426        blueprint,
427        config,
428    );
429}
430
431#[cube]
432fn reduce_with_indices_kernel_inner<
433    P: ReducePrecision,
434    Out: NumericVector,
435    Idx: NumericVector,
436    R: ReduceWithIndicesFamily,
437>(
438    input: &VirtualTensor<P::EI, P::SI>,
439    output: &mut VirtualTensor<Out::T, Out::N, ReadWrite>,
440    indices: &mut VirtualTensor<Idx::T, Idx::N, ReadWrite>,
441    reduce_axis: usize,
442    out_vec_axis: usize,
443    #[comptime] blueprint: ReduceBlueprint,
444    #[comptime] config: R::Config,
445) {
446    let inst = R::Instruction::<P>::from_config(config);
447
448    match blueprint.global {
449        GlobalReduceBlueprint::Cube(cube) => {
450            GlobalFullCubeReduce::execute_with_indices::<P, Out, Idx, R::Instruction<P>>(
451                input,
452                output,
453                indices,
454                reduce_axis,
455                out_vec_axis,
456                &inst,
457                blueprint.vectorization_mode,
458                cube,
459            )
460        }
461        GlobalReduceBlueprint::Plane(plane) => {
462            GlobalFullPlaneReduce::execute_with_indices::<P, Out, Idx, R::Instruction<P>>(
463                input,
464                output,
465                indices,
466                reduce_axis,
467                out_vec_axis,
468                &inst,
469                blueprint.vectorization_mode,
470                plane,
471            )
472        }
473        GlobalReduceBlueprint::Unit(unit) => {
474            GlobalFullUnitReduce::execute_with_indices::<P, Out, Idx, R::Instruction<P>>(
475                input,
476                output,
477                indices,
478                reduce_axis,
479                out_vec_axis,
480                &inst,
481                blueprint.vectorization_mode,
482                unit,
483            )
484        }
485    };
486}
487
488#[cube]
489pub fn reduce_kernel_virtual<
490    In: Numeric,
491    InSize: Size,
492    Out: Numeric,
493    OutSize: Size,
494    Acc: Numeric,
495>(
496    input: &VirtualTensor<In, InSize>,
497    output: &mut VirtualTensor<Out, OutSize, ReadWrite>,
498    reduce_axis: usize,
499    out_vec_axis: usize,
500    #[comptime] blueprint: ReduceBlueprint,
501    #[comptime] config: ReduceOperationConfig,
502) {
503    reduce_kernel_inner::<(In, InSize, Acc), (Out, OutSize), ReduceOperation>(
504        input,
505        output,
506        reduce_axis,
507        out_vec_axis,
508        blueprint,
509        config,
510    )
511}
512
513#[cube]
514fn reduce_kernel_inner<P: ReducePrecision, Out: NumericVector, R: ReduceFamily>(
515    input: &VirtualTensor<P::EI, P::SI>,
516    output: &mut VirtualTensor<Out::T, Out::N, ReadWrite>,
517    reduce_axis: usize,
518    out_vec_axis: usize,
519    #[comptime] blueprint: ReduceBlueprint,
520    #[comptime] config: R::Config,
521) {
522    let inst = R::Instruction::<P>::from_config(config);
523
524    match blueprint.global {
525        GlobalReduceBlueprint::Cube(cube) => {
526            GlobalFullCubeReduce::execute::<P, Out, R::Instruction<P>>(
527                input,
528                output,
529                reduce_axis,
530                out_vec_axis,
531                &inst,
532                blueprint.vectorization_mode,
533                cube,
534            )
535        }
536        GlobalReduceBlueprint::Plane(plane) => {
537            GlobalFullPlaneReduce::execute::<P, Out, R::Instruction<P>>(
538                input,
539                output,
540                reduce_axis,
541                out_vec_axis,
542                &inst,
543                blueprint.vectorization_mode,
544                plane,
545            )
546        }
547        GlobalReduceBlueprint::Unit(unit) => {
548            GlobalFullUnitReduce::execute::<P, Out, R::Instruction<P>>(
549                input,
550                output,
551                reduce_axis,
552                out_vec_axis,
553                &inst,
554                blueprint.vectorization_mode,
555                unit,
556            )
557        }
558    };
559}