use std::marker::PhantomData;
use std::sync::Arc;
use cubecl::frontend::{AsMutExpand, AsRefExpand, CubeDebug, ExpandTypeClone, IntoExpand, IntoMut};
use cubecl::frontend::{buffer_len::expand_buffer_length_native, slice};
use cubecl::ir::pliron::value::Value;
use cubecl::ir::{ElemType, Scope, types::VectorType};
use cubecl::prelude::*;
use cubecl::unexpanded;
use cubecl_std::tensor::{
layout::{CoordsDyn, VirtualLayout, VirtualLayoutExpand},
view::launch::{VirtualViewLayoutCompilationArg, VirtualViewLayoutLaunch},
};
use crate::launch::binding::rebound_tensor;
use crate::tile::packing::base::Packing;
use crate::*;
use cubecl::server::BufferBinding;
pub struct TileArg<'a> {
pub spec: TileSpec,
pub element: ElemType,
_values: PhantomData<&'a ()>,
}
impl TileArg<'_> {
pub fn tile(&self, _partitioning: &Partitioning) -> Tile {
unexpanded!()
}
pub fn lines<E: Numeric, V: Size>(&self) -> &Tensor<Vector<E, V>> {
unexpanded!()
}
}
#[cube]
fn buffer_tile<E: Numeric>(
tensor: &Tensor<E>,
partitioning: &Partitioning,
coefficients: Coords<u32>,
offsets: Coords<i32>,
#[comptime] spec: TileSpec,
) -> Tile {
let space = comptime!(partitioning.clone());
GlobalOperand::gathered(
tensor,
comptime!(space.space().clone()),
spec,
coefficients,
offsets,
)
.tile::<E>(comptime!(space.levels().to_vec()))
}
#[cube]
fn table_tile<E: Numeric>(
tensor: &Tensor<E>,
#[comptime] entries: usize,
#[comptime] spec: TileSpec,
) -> Tile {
let space = comptime!(Space::new(&[(spec.axes()[0], entries)]));
GlobalOperand::tensor(tensor, comptime!(space.clone()), spec).tile::<E>(comptime!(Vec::new()))
}
#[cube]
fn paged_tile<E: Numeric>(
tensor: &Tensor<E>,
table: &Tensor<u32>,
partitioning: &Partitioning,
#[comptime] spec: TileSpec,
) -> Tile {
let space = comptime!(partitioning.clone());
GlobalOperand::paged(tensor, table, comptime!(space.space().clone()), spec)
.tile::<E>(comptime!(space.levels().to_vec()))
}
#[cube]
fn tensor_map_tile<E: Numeric>(
map: TensorMap<E, Tiled>,
layout: VirtualLayout<CoordsDyn, Sequence<i32>>,
partitioning: &Partitioning,
#[comptime] spec: TileSpec,
) -> Tile {
let space = comptime!(partitioning.clone());
let own = comptime!(space.space().subspace(spec.axes()));
let data = TmaData::from_tensor_map(
map,
layout,
comptime!(own.rank()),
comptime!(spec.units),
comptime!(spec.packing),
);
Tile::new::<E>(
TileKind::new_TmaGmem(data),
comptime!(Placement::root(own.clone(), space.levels().to_vec())),
)
}
#[derive(Clone)]
pub struct TileArgExpand<'a> {
values: ValuesExpand<'a>,
pub spec: TileSpec,
pub element: ElemType,
scale: Option<Box<TileArgExpand<'static>>>,
index: Option<(Box<TileArgExpand<'static>>, Indexing)>,
codebook: Option<Box<TileArgExpand<'static>>>,
runtime_map: (CoordsExpand<u32>, CoordsExpand<i32>),
}
#[derive(Clone)]
enum ValuesExpand<'a> {
Tensor {
tensor: &'a TensorExpand<Held>,
buffer: Value,
meta: TensorMetaExpand,
width: usize,
},
TensorMap {
map: Value,
layout: VirtualLayoutExpand<CoordsDyn, Sequence<i32>>,
},
}
impl<'a> TileArgExpand<'a> {
pub fn __expand_tile_method(
&self,
scope: &Scope,
partitioning: &PartitioningExpand,
) -> TileExpand {
let values = self.values_tile(scope, partitioning);
let values = self.looking_up(scope, values);
let values = self.indexing(scope, values, partitioning);
self.scaling(scope, values, partitioning)
}
pub fn __expand_lines_method<E: Numeric, V: Size>(
&self,
scope: &Scope,
) -> &'a TensorExpand<Vector<E, V>> {
let ValuesExpand::Tensor {
buffer,
meta,
width: bound,
..
} = &self.values
else {
panic!("TileArg::lines: a tensor map has no lines to add into")
};
let named = E::elem_type(scope);
assert_eq!(
named, self.element,
"TileArg::lines: the buffer is bound at {}, not {named}",
self.element
);
let width = V::__expand_value(scope);
assert_eq!(
width, *bound,
"TileArg::lines: the buffer is bound {bound} wide, not {width}"
);
let len = expand_buffer_length_native(scope, *buffer);
let lines = slice::from_raw_parts::<Vector<E, V>>(
scope,
*buffer,
0usize.into_expand(scope),
len.into(),
);
scope.create_kernel_ref(TensorExpand::__expand_from_parts(*meta, lines))
}
fn values_tile(&self, scope: &Scope, partitioning: &PartitioningExpand) -> TileExpand {
let _held = scope.define_type::<Held>(self.element);
match &self.values {
ValuesExpand::Tensor { tensor, meta, .. } => {
let tensor = scope.create_kernel_ref(TensorExpand::<Held>::__expand_from_parts(
*meta,
tensor.expand.into(),
));
if let (Some(_), Some((table, _))) = (&self.spec.paging, &self.index) {
let ValuesExpand::Tensor {
tensor: pages,
meta,
..
} = &table.values
else {
unreachable!("TileArgLaunch::indexed binds its table as a buffer")
};
let pages = scope.create_kernel_ref(TensorExpand::<u32>::__expand_from_parts(
*meta,
pages.expand.into(),
));
return paged_tile::expand::<Held>(
scope,
tensor,
pages,
partitioning,
self.spec.clone(),
);
}
let (coefficients, offsets) = self.runtime_map.clone();
buffer_tile::expand::<Held>(
scope,
tensor,
partitioning,
coefficients,
offsets,
self.spec.clone(),
)
}
ValuesExpand::TensorMap { map, layout } => tensor_map_tile::expand::<Held>(
scope,
(*map).into(),
layout.clone(),
partitioning,
self.spec.clone(),
),
}
}
fn looking_up(&self, scope: &Scope, values: TileExpand) -> TileExpand {
let Some(table) = &self.codebook else {
return values;
};
let ValuesExpand::Tensor { tensor, meta, .. } = &table.values else {
unreachable!("TileArgLaunch::codebook binds a buffer")
};
let Packing::Packed {
field: Field::Index { bits },
} = self.spec.packing
else {
unreachable!("TileArgLaunch::codebook binds values stored as indices")
};
let _held = scope.define_type::<Held>(table.element);
let entries = scope.create_kernel_ref(TensorExpand::<Held>::__expand_from_parts(
*meta,
tensor.expand.into(),
));
let table = table_tile::expand::<Held>(scope, entries, 1 << bits, table.spec.clone());
values.__expand_lookup_method(scope, &table)
}
fn indexing(
&self,
scope: &Scope,
values: TileExpand,
partitioning: &PartitioningExpand,
) -> TileExpand {
let Some((table, indexing)) = &self.index else {
return values;
};
if self.spec.paging.is_some() {
return values;
}
let keys = table.spec.axes().to_vec();
let read_at = indexing
.read_at(&keys, &partitioning.levels)
.unwrap_or_else(|refusal| panic!("{refusal}"));
let ValuesExpand::Tensor { tensor, meta, .. } = &table.values else {
panic!("{}", Refusal::IndexTableNotU32)
};
let entries = scope.create_kernel_ref(TensorExpand::<u32>::__expand_from_parts(
*meta,
tensor.expand.into(),
));
let table = IndexTable::__expand_new(scope, entries, keys, indexing.clone());
let values = values.__expand_with_index_method(scope, table);
match read_at {
Some(_) => values,
None => values.__expand_indexed_whole_method(scope),
}
}
fn scaling(
&self,
scope: &Scope,
values: TileExpand,
partitioning: &PartitioningExpand,
) -> TileExpand {
let Some(level) = &self.scale else {
return values;
};
let words =
level.element == u32::elem_type_native() && level.spec.packing == Packing::Plain;
assert!(
words || !matches!(self.values, ValuesExpand::TensorMap { .. }),
"TileArg: a tensor map lands its values as they lie, so it rides only scales a stage \
keeps beside them: words of block scales"
);
values.__expand_mul_method(scope, &level.__expand_tile_method(scope, partitioning))
}
}
pub struct TileArgLaunch {
values: ValuesLaunch,
spec: Arc<TileSpec>,
element: ElemType,
scale: Option<Box<TileArgLaunch>>,
index: Option<(Box<TileArgLaunch>, Indexing)>,
codebook: Option<Box<TileArgLaunch>>,
runtime_map: (Vec<u32>, Vec<i32>),
}
enum ValuesLaunch {
Tensor {
tensor: TensorArg,
width: usize,
},
TensorMap {
map: Box<TensorMapArg<Tiled>>,
layout: VirtualViewLayoutLaunch<CoordsDyn, Sequence<i32>, TensorMapArg<Tiled>>,
},
}
impl TileArgLaunch {
pub fn new(tensor: TensorArg, element: ElemType, width: usize, spec: TileSpec) -> Self {
Self::of(ValuesLaunch::Tensor { tensor, width }, element, spec)
}
pub(crate) fn of_tensor_map(
map: TensorMapArg<Tiled>,
layout: VirtualViewLayoutLaunch<CoordsDyn, Sequence<i32>, TensorMapArg<Tiled>>,
spec: TileSpec,
) -> Self {
let element = map.metadata.elem_ty;
let map = Box::new(map);
Self::of(ValuesLaunch::TensorMap { map, layout }, element, spec)
}
fn of(values: ValuesLaunch, element: ElemType, spec: TileSpec) -> Self {
assert!(
spec.packing == Packing::Plain || element == u32::elem_type_native(),
"TileArgLaunch: a packed operand is bound at the u32 words its buffer holds, not \
{element:?}"
);
Self {
values,
spec: Arc::new(spec),
element,
scale: None,
index: None,
codebook: None,
runtime_map: (Vec::new(), Vec::new()),
}
}
pub fn rebound(&self, buffers: &mut impl Iterator<Item = BufferBinding>) -> Option<Self> {
let values = match &self.values {
ValuesLaunch::Tensor { tensor, width } => ValuesLaunch::Tensor {
tensor: rebound_tensor(tensor, buffers.next()?)?,
width: *width,
},
ValuesLaunch::TensorMap { .. } => return None,
};
let scale = match &self.scale {
Some(scale) => Some(Box::new(scale.rebound(buffers)?)),
None => None,
};
let index = match &self.index {
Some((table, indexing)) => Some((Box::new(table.rebound(buffers)?), indexing.clone())),
None => None,
};
let codebook = match &self.codebook {
Some(table) => Some(Box::new(table.rebound(buffers)?)),
None => None,
};
Some(Self {
values,
spec: self.spec.clone(),
element: self.element,
scale,
index,
codebook,
runtime_map: self.runtime_map.clone(),
})
}
pub fn codebook(mut self, table: TileArgLaunch) -> Result<Self, Refusal> {
let Packing::Packed {
field: Field::Index { bits },
} = self.spec.packing
else {
return Err(Refusal::CodebookOfValuesNotIndices);
};
let ValuesLaunch::Tensor { tensor, width } = &table.values else {
return Err(Refusal::CodebookTableNotPlain);
};
let plain = *width == 1
&& tensor.shape().len() == 1
&& table.spec.axes().len() == 1
&& table.spec.packing == Packing::Plain
&& table.scale.is_none()
&& table.index.is_none()
&& table.codebook.is_none();
if !plain {
return Err(Refusal::CodebookTableNotPlain);
}
let entries = tensor.shape()[0];
if entries < 1 << bits {
return Err(Refusal::CodebookTableShort { entries, bits });
}
self.codebook = Some(Box::new(table));
Ok(self)
}
pub fn runtime_map(mut self, coefficients: Vec<u32>, offsets: Vec<i32>) -> Self {
self.runtime_map = (coefficients, offsets);
self
}
pub fn scales(mut self, scale: TileArgLaunch) -> Self {
self.scale = Some(Box::new(match self.scale {
None => scale,
Some(inner) => inner.scales(scale),
}));
self
}
pub fn indexed(self, table: TileArgLaunch, indexing: Indexing) -> Result<Self, Refusal> {
self.placed_by(table, indexing, false)
}
pub fn indexed_covering(
self,
table: TileArgLaunch,
indexing: Indexing,
) -> Result<Self, Refusal> {
self.placed_by(table, indexing, true)
}
fn placed_by(
mut self,
table: TileArgLaunch,
indexing: Indexing,
covering: bool,
) -> Result<Self, Refusal> {
let ValuesLaunch::Tensor { tensor, width } = &table.values else {
return Err(Refusal::IndexTableNotU32);
};
let plain_u32 = table.element == u32::elem_type_native()
&& *width == 1
&& table.spec.packing == Packing::Plain
&& table.scale.is_none()
&& table.index.is_none();
if !plain_u32 {
return Err(Refusal::IndexTableNotU32);
}
let keys = table.spec.axes();
let rank = tensor.shape().len();
if rank != keys.len() {
return Err(Refusal::IndexTableRank {
keys: keys.len(),
rank,
});
}
if let (Indexing::Coordinate(sequence), &[first, position]) = (&indexing, keys)
&& first == *sequence
{
self.spec = Arc::new(self.paged_spec(*sequence, position, covering)?);
let mut level = self.scale.as_deref();
while let Some(scale) = level {
let spans = |axis| scale.spec.axes().contains(&axis);
if (spans(first) || spans(position)) && scale.spec.paging.is_none() {
return Err(Refusal::ScalesOfAPoolUnpaged);
}
level = scale.scale.as_deref();
}
self.index = Some((Box::new(table), indexing));
return Ok(self);
}
indexing.validate(&self.spec, self.indexed_width()?, keys)?;
let mut level = self.scale.as_deref();
while let Some(scale) = level {
if scale.spec.axes().contains(&indexing.axis()) {
indexing.places(&scale.spec, scale.indexed_width()?)?;
}
level = scale.scale.as_deref();
}
self.index = Some((Box::new(table), indexing));
Ok(self)
}
fn paged_spec(
&self,
sequence: Axis,
position: Axis,
covering: bool,
) -> Result<TileSpec, Refusal> {
let ValuesLaunch::Tensor { tensor, .. } = &self.values else {
return Err(Refusal::PagingMovedByRuns);
};
let mut spec = TileSpec::clone(&self.spec);
let paging = Paging::of_pool(
sequence,
position,
tensor.shape(),
&spec.projection,
&spec.stored_tiles,
spec.delivery,
)?;
if !covering {
let boundary = spec.boundaries.iter().flatten().next().copied();
if spec.boundaries.is_empty() {
spec.boundaries = (0..spec.projection.coordinate_rank())
.map(|_| None)
.collect();
}
let (sequence_dim, position_dim) = paging.dims(&spec.projection)?;
for dim in [sequence_dim, position_dim] {
spec.boundaries[dim] = Some(boundary.unwrap_or(Boundary::Zero));
}
}
spec.paging = Some(paging);
Ok(spec)
}
fn indexed_width(&self) -> Result<usize, Refusal> {
match &self.values {
ValuesLaunch::Tensor { width, .. } => Ok(*width),
ValuesLaunch::TensorMap { .. } => Err(Refusal::IndexOfAStatedLayout),
}
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct TileArgCompilationArg {
values: ValuesCompilationArg,
spec: Arc<TileSpec>,
element: ElemType,
scale: Option<Box<TileArgCompilationArg>>,
index: Option<(Box<TileArgCompilationArg>, Indexing)>,
codebook: Option<Box<TileArgCompilationArg>>,
runtime_map: (usize, usize),
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum ValuesCompilationArg {
Tensor {
tensor: TensorCompilationArg,
width: usize,
},
TensorMap(VirtualViewLayoutCompilationArg<CoordsDyn, Sequence<i32>>),
}
impl<'a> CubeType for TileArg<'a> {
type ExpandType = TileArgExpand<'a>;
}
impl LaunchArg for TileArg<'static> {
type RuntimeArg = TileArgLaunch;
type CompilationArg = TileArgCompilationArg;
fn register(arg: Self::RuntimeArg, launcher: &mut KernelLauncher) -> Self::CompilationArg {
let TileArgLaunch {
values,
spec,
element,
scale,
index,
codebook,
runtime_map: (coefficients, offsets),
} = arg;
let values = match values {
ValuesLaunch::Tensor { tensor, width } => {
let line_size = element.size() * width;
let meta = TensorMetaLaunch::new(tensor.size() / width, tensor.shape().len());
let buffer = match &tensor {
TensorArg::Handle { .. } => BufferCompilationArg { inplace: None },
TensorArg::Alias { input_pos, .. } => BufferCompilationArg {
inplace: Some(*input_pos),
},
};
launcher.register_tensor(tensor, line_size);
let meta = TensorMeta::register(meta, launcher);
ValuesCompilationArg::Tensor {
tensor: TensorCompilationArg { meta, buffer },
width,
}
}
ValuesLaunch::TensorMap { map, layout } => {
let layout = layout.register(&map, Type::new(element), launcher);
launcher.register_tensor_map(*map, element.size());
ValuesCompilationArg::TensorMap(layout)
}
};
let scale = scale.map(|level| Box::new(TileArg::register(*level, launcher)));
let index = index
.map(|(table, indexing)| (Box::new(TileArg::register(*table, launcher)), indexing));
let codebook = codebook.map(|table| Box::new(TileArg::register(*table, launcher)));
for coefficient in &coefficients {
coefficient.register(launcher);
}
for offset in &offsets {
offset.register(launcher);
}
TileArgCompilationArg {
values,
spec,
element,
scale,
index,
codebook,
runtime_map: (coefficients.len(), offsets.len()),
}
}
fn expand(arg: &Self::CompilationArg, builder: &mut KernelBuilder) -> TileArgExpand<'static> {
let values = match &arg.values {
ValuesCompilationArg::Tensor { tensor, width } => {
let buffer = match tensor.buffer.inplace {
Some(id) => builder.inplace(id),
None => {
let scope = &builder.scope;
let held = arg.element.to_type(scope.ctx());
let line = match *width {
1 => held,
width => VectorType::get(scope.ctx(), held, width).into(),
};
builder.tensor(line)
}
};
let meta = TensorMeta::expand(&tensor.meta, builder);
let scope = &builder.scope;
let _held = scope.define_type::<Held>(arg.element);
let len = expand_buffer_length_native(scope, buffer);
let values = slice::from_raw_parts::<Held>(
scope,
buffer,
0usize.into_expand(scope),
len.into(),
);
ValuesExpand::Tensor {
tensor: scope
.create_kernel_ref(TensorExpand::__expand_from_parts(meta, values)),
buffer,
meta,
width: *width,
}
}
ValuesCompilationArg::TensorMap(layout) => ValuesExpand::TensorMap {
layout: layout.expand(Type::new(arg.element), builder),
map: builder.tensor_map(),
},
};
let scale = arg
.scale
.as_ref()
.map(|level| Box::new(TileArg::expand(level, builder)));
let index = arg
.index
.as_ref()
.map(|(table, indexing)| (Box::new(TileArg::expand(table, builder)), indexing.clone()));
let codebook = arg
.codebook
.as_ref()
.map(|table| Box::new(TileArg::expand(table, builder)));
let (coefficients, offsets) = arg.runtime_map;
let mut runtime_map = (
Coords::<u32>::__expand_new(&builder.scope),
Coords::<i32>::__expand_new(&builder.scope),
);
for _ in 0..coefficients {
let coefficient = u32::expand_scalar(builder);
runtime_map
.0
.__expand_push_method(&builder.scope, coefficient);
}
for _ in 0..offsets {
let offset = i32::expand_scalar(builder);
runtime_map.1.__expand_push_method(&builder.scope, offset);
}
TileArgExpand {
values,
spec: TileSpec::clone(&arg.spec),
element: arg.element,
scale,
index,
codebook,
runtime_map,
}
}
}
impl IntoExpand for TileArgExpand<'_> {
type Expand = Self;
fn into_expand(self, _: &Scope) -> Self {
self
}
}
impl ExpandTypeClone for TileArgExpand<'_> {
fn clone_unchecked(&self) -> Self {
self.clone()
}
}
impl IntoMut for TileArgExpand<'_> {
fn into_mut(self, _: &Scope) -> Self {
self
}
}
impl CubeDebug for TileArgExpand<'_> {}
impl AsRefExpand for TileArgExpand<'_> {
fn __expand_ref_method(&self, _: &Scope) -> &Self {
self
}
}
impl AsMutExpand for TileArgExpand<'_> {
fn __expand_ref_mut_method(&mut self, _: &Scope) -> &mut Self {
self
}
}
#[cfg(test)]
mod tests {
use cubecl::ir::FloatKind;
use super::*;
fn expanded_f32_buffer(builder: &mut KernelBuilder) -> TileArgExpand<'static> {
let mut launcher = KernelLauncher::new(builder_settings());
let meta = TensorMeta::register(TensorMetaLaunch::new(4, 1), &mut launcher);
let arg = TileArgCompilationArg {
values: ValuesCompilationArg::Tensor {
tensor: TensorCompilationArg {
meta,
buffer: BufferCompilationArg { inplace: None },
},
width: 1,
},
spec: Arc::new(TileSpec::direct(&[Axis(0)])),
element: ElemType::Float(FloatKind::F32),
scale: None,
index: None,
codebook: None,
runtime_map: (0, 0),
};
TileArg::expand(&arg, builder)
}
fn builder_settings() -> KernelSettings {
KernelSettings::new(
cubecl::ir::settings::Dim3::new_single(),
cubecl::ir::settings::ExecutionMode::Checked,
cubecl::ir::AddressType::U32,
)
}
#[test]
#[should_panic(expected = "TileArg::lines: the buffer is bound at f32, not i32")]
fn lines_refuse_a_buffer_bound_at_another_element() {
let mut builder = KernelBuilder::new(builder_settings());
let arg = expanded_f32_buffer(&mut builder);
arg.__expand_lines_method::<i32, Const<1>>(&builder.scope);
}
fn expanded_table(builder: &mut KernelBuilder, keys: &[Axis]) -> TileArgExpand<'static> {
let mut launcher = KernelLauncher::new(builder_settings());
let meta = TensorMeta::register(TensorMetaLaunch::new(4, keys.len()), &mut launcher);
let arg = TileArgCompilationArg {
values: ValuesCompilationArg::Tensor {
tensor: TensorCompilationArg {
meta,
buffer: BufferCompilationArg { inplace: None },
},
width: 1,
},
spec: Arc::new(TileSpec::direct(keys)),
element: u32::elem_type_native(),
scale: None,
index: None,
codebook: None,
runtime_map: (0, 0),
};
TileArg::expand(&arg, builder)
}
fn serve_indexed(keys: &[Axis], space: &[(Axis, usize)], levels: Vec<Level>) {
let mut builder = KernelBuilder::new(builder_settings());
let mut values = expanded_f32_buffer(&mut builder);
let table = expanded_table(&mut builder, keys);
values.index = Some((Box::new(table), Indexing::Run(Axis(0))));
let scope = &builder.scope;
let partitioning = PartitioningExpand {
space: SpaceExpand {
shape: Space::new(space).shape().clone(),
sizes: Sequence::<usize>::__expand_new(scope),
},
levels,
};
values.__expand_tile_method(scope, &partitioning);
}
#[test]
#[should_panic(
expected = "the first level cutting one of [Axis(1), Axis(2)] does not cut them all"
)]
fn keys_cut_at_two_levels_are_refused_as_the_tile_expands() {
serve_indexed(
&[Axis(1), Axis(2)],
&[(Axis(0), 4), (Axis(1), 2), (Axis(2), 2)],
vec![Level::every(&[(Axis(1), 1)]), Level::every(&[(Axis(2), 1)])],
);
}
#[test]
fn a_table_no_level_cuts_is_read_as_the_tile_expands() {
serve_indexed(
&[Axis(1)],
&[(Axis(0), 4), (Axis(1), 1)],
vec![Level::every(&[(Axis(0), 2)])],
);
}
#[test]
fn lines_read_a_buffer_at_the_element_it_is_bound_at() {
let mut builder = KernelBuilder::new(builder_settings());
let arg = expanded_f32_buffer(&mut builder);
arg.__expand_lines_method::<f32, Const<1>>(&builder.scope);
}
}