Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions diskann-quantization/src/multi_vector/distance/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ use diskann_wide::arch::x86_64::{V3, V4};

use super::isa::{MaxSimIsa, NotSupported};
use super::kernel::{Erase, MaxSimKernel};
use super::kernels::f16::F16Entry;
use super::kernels::f32::F32Kernel;
use super::kernels::{MaxIp, MaxIpF16};
use super::max_sim::{MaxSim, MaxSimError};
use crate::multi_vector::distance::QueryMatRef;
use crate::multi_vector::{BlockTransposed, BlockTransposedRef, Mat, MatRef, Standard};
Expand All @@ -35,7 +34,7 @@ struct Prepared<A, Q> {
impl<A, const GROUP: usize> MaxSimKernel<f32> for Prepared<A, BlockTransposed<f32, GROUP>>
where
A: Architecture,
F32Kernel<GROUP>: for<'a> diskann_wide::arch::Target3<
MaxIp: for<'a> diskann_wide::arch::Target3<
A,
(),
BlockTransposedRef<'a, f32, GROUP>,
Expand All @@ -59,14 +58,11 @@ where
scores.fill(f32::MAX);
return Ok(());
}
let mut scratch = vec![f32::MIN; self.prepared.padded_nrows()];
self.arch.run3(
F32Kernel::<GROUP>,
self.prepared.reborrow(),
doc,
&mut scratch,
);
for (dst, &src) in scores.iter_mut().zip(&scratch[..self.prepared.nrows()]) {
// `run` seeds the max itself, so the fill value here is arbitrary.
let mut state = vec![0.0; self.prepared.padded_nrows()];
self.arch
.run3(MaxIp, self.prepared.reborrow(), doc, &mut state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call no longer performs the old query.ncols() == doc.vector_dim() boundary check. For non-empty contractions, a mismatch reaches the opaque "B panel extent" assertion; for zero-dimensional queries or empty documents, the early returns accept the mismatch and return scores. Please validate dimensions before either early return for both f32 and f16, and report both values.

for (dst, &src) in scores.iter_mut().zip(&state[..self.prepared.nrows()]) {
*dst = -src;
}
Ok(())
Expand All @@ -77,7 +73,7 @@ impl<A, const GROUP: usize> MaxSimKernel<half::f16>
for Prepared<A, BlockTransposed<half::f16, GROUP>>
where
A: Architecture,
F16Entry<GROUP>: for<'a> diskann_wide::arch::Target3<
MaxIpF16: for<'a> diskann_wide::arch::Target3<
A,
(),
BlockTransposedRef<'a, half::f16, GROUP>,
Expand All @@ -101,14 +97,11 @@ where
scores.fill(f32::MAX);
return Ok(());
}
let mut scratch = vec![f32::MIN; self.prepared.padded_nrows()];
self.arch.run3(
F16Entry::<GROUP>,
self.prepared.reborrow(),
doc,
&mut scratch,
);
for (dst, &src) in scores.iter_mut().zip(&scratch[..self.prepared.nrows()]) {
// `run` seeds the max itself, so the fill value here is arbitrary.
let mut state = vec![0.0; self.prepared.padded_nrows()];
self.arch
.run3(MaxIpF16, self.prepared.reborrow(), doc, &mut state);
for (dst, &src) in scores.iter_mut().zip(&state[..self.prepared.nrows()]) {
*dst = -src;
}
Ok(())
Expand Down
207 changes: 176 additions & 31 deletions diskann-quantization/src/multi_vector/distance/kernels/f16.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,197 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

//! f16 dispatch adapter for block-transposed multi-vector distance.
//! f16 MaxSim.
//!
//! Reuses the f32 micro-kernel family with tile-level f16→f32 conversion
//! via [`ConvertTo`](super::layouts::ConvertTo). No f16-specific micro-kernel
//! code is needed — the [`F32Kernel`](super::f32::F32Kernel) does all the
//! SIMD work after conversion.
//!
//! Conversion from f16 to f32 is performed at tile granularity via
//! [`SliceCast`](diskann_vector::conversion::SliceCast), dispatched through
//! the runtime architecture token — the same SIMD level used by the
//! micro-kernel.
//! There is no f16 leaf: f16 widens to f32 and reuses the f32 pipeline. Both sides widen a
//! tile at a time into a buffer the walk reuses, which is what the lending [`TileWalk`]
//! exists for — the whole query never has to be staged at once, and the staged copy stays
//! inside the cache level its tile was sized for.

use diskann_vector::conversion::SliceCast;
use diskann_wide::Architecture;
#[cfg(target_arch = "x86_64")]
use diskann_wide::arch::x86_64::V3;
use diskann_wide::arch::{Scalar, Target2};

use super::Kernel;
use super::TileBudget;
use super::f32::{F32Kernel, max_ip_kernel};
use super::layouts;
use super::leaves::scalar::{A_PANEL as SC_A, B_PANEL as SC_B};
#[cfg(target_arch = "x86_64")]
use super::leaves::v3::{A_PANEL as V3_A, B_PANEL as V3_B};
use super::tiles::{Cursor, DocTile, QueryTile};
use super::{Plan, TileAt, TileBudget, TileWalk, float};
use crate::multi_vector::{BlockTransposedRef, MatRef, Standard};

pub(crate) struct F16Entry<const GROUP: usize>;
/// Stages one source tile at a time as f32.
struct Widen<'a, Arch> {
arch: Arch,
cursor: Cursor<'a, half::f16>,
buf: Vec<f32>,
k: usize,
}

impl<'a, Arch: Architecture> Widen<'a, Arch>
where
SliceCast<f32, half::f16>: for<'x> Target2<Arch, (), &'x mut [f32], &'x [half::f16]>,
{
/// # Panics
///
/// Panics if `k` is zero — the entry guards that case before any walk is built.
fn new(arch: Arch, src: &'a [half::f16], k: usize, stride: usize) -> Self {
assert!(k > 0, "widening walk requires a non-empty contraction");
let cursor = Cursor::new(src, stride);
let buf = vec![0.0f32; cursor.widest()];
Self {
arch,
cursor,
buf,
k,
}
}

fn next(&mut self) -> Option<&[f32]> {
let arch = self.arch;
let src = self.cursor.next()?;
let len = src.len();
arch.run2(SliceCast::new(), &mut self.buf[..len], src);
Some(&self.buf[..len])
}
}

/// Widens the padded storage of a block-transposed f16 query.
///
/// Widening is element-wise, so it preserves the block-transposed permutation.
struct QueryWiden<'a, Arch, const AR: usize>(Widen<'a, Arch>);

impl<'a, Arch: Architecture, const AR: usize> QueryWiden<'a, Arch, AR>
where
SliceCast<f32, half::f16>: for<'x> Target2<Arch, (), &'x mut [f32], &'x [half::f16]>,
{
fn new(arch: Arch, view: BlockTransposedRef<'a, half::f16, AR>, a_panels: usize) -> Self {
let k = view.padded_ncols();
Self(Widen::new(arch, view.as_slice(), k, a_panels * AR * k))
}
}

impl<'t, Arch, const AR: usize> TileAt<'t> for QueryWiden<'_, Arch, AR> {
type Tile = QueryTile<'t, f32, AR>;
}

impl<Arch: Architecture, const AR: usize> TileWalk for QueryWiden<'_, Arch, AR>
where
SliceCast<f32, half::f16>: for<'x> Target2<Arch, (), &'x mut [f32], &'x [half::f16]>,
{
fn next(&mut self) -> Option<QueryTile<'_, f32, AR>> {
let k = self.0.k;
self.0.next().map(|data| QueryTile::new(data, k))
}

fn reset(&mut self) {
self.0.cursor.reset();
}
}

/// Widens a row-major f16 doc matrix.
struct DocWiden<'a, Arch, const BR: usize>(Widen<'a, Arch>);

impl<'a, Arch: Architecture, const BR: usize> DocWiden<'a, Arch, BR>
where
SliceCast<f32, half::f16>: for<'x> Target2<Arch, (), &'x mut [f32], &'x [half::f16]>,
{
fn new(arch: Arch, docs: MatRef<'a, Standard<half::f16>>, b_panels: usize) -> Self {
let k = docs.vector_dim();
Self(Widen::new(arch, docs.as_slice(), k, b_panels * BR * k))
}
}

impl<'t, Arch, const BR: usize> TileAt<'t> for DocWiden<'_, Arch, BR> {
type Tile = DocTile<'t, f32, BR>;
}

impl<Arch: Architecture, const BR: usize> TileWalk for DocWiden<'_, Arch, BR>
where
SliceCast<f32, half::f16>: for<'x> Target2<Arch, (), &'x mut [f32], &'x [half::f16]>,
{
fn next(&mut self) -> Option<DocTile<'_, f32, BR>> {
let k = self.0.k;
self.0.next().map(|data| DocTile::new(data, k))
}

fn reset(&mut self) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactor removes the old asymmetric multi-tile f16 coverage, while the replacement tiny-budget tests only exercise the f32 walks. This leaves the new reusable widening buffer and RowMajorWiden::reset path untested. Please add forced multi-tile f16 Scalar and V3 cases against the naive reference, including asymmetric A/B tile counts.

self.0.cursor.reset();
}
}

// ── Entry ────────────────────────────────────────────────────────

/// The f16 MaxSim entry — the f32 pipeline behind widening walks.
#[derive(Debug, Clone, Copy)]
pub(crate) struct MaxIpF16;

impl<A, const GROUP: usize>
#[cfg(target_arch = "x86_64")]
impl
diskann_wide::arch::Target3<
A,
V3,
(),
BlockTransposedRef<'_, half::f16, GROUP>,
BlockTransposedRef<'_, half::f16, V3_A>,
MatRef<'_, Standard<half::f16>>,
&mut [f32],
> for F16Entry<GROUP>
where
A: Architecture,
F32Kernel<GROUP>: Kernel<A>,
layouts::BlockTransposed<half::f16, GROUP>: layouts::ConvertTo<A, <F32Kernel<GROUP> as Kernel<A>>::Left>
+ layouts::Layout<Element = half::f16>,
layouts::RowMajor<half::f16>: layouts::ConvertTo<A, <F32Kernel<GROUP> as Kernel<A>>::Right>
+ layouts::Layout<Element = half::f16>,
> for MaxIpF16
{
#[inline(always)]
fn run(
self,
arch: V3,
query: BlockTransposedRef<'_, half::f16, V3_A>,
docs: MatRef<'_, Standard<half::f16>>,
state: &mut [f32],
) {
float::run(
arch,
query.padded_nrows(),
docs.num_vectors(),
query.padded_ncols(),
TileBudget::default(),
state,
|plan: Plan<V3_A, V3_B>| {
(
QueryWiden::new(arch, query, plan.a_panels),
DocWiden::new(arch, docs, plan.b_panels),
)
},
);
}
}

impl
diskann_wide::arch::Target3<
Scalar,
(),
BlockTransposedRef<'_, half::f16, SC_A>,
MatRef<'_, Standard<half::f16>>,
&mut [f32],
> for MaxIpF16
{
#[inline(always)]
fn run(
self,
arch: A,
lhs: BlockTransposedRef<'_, half::f16, GROUP>,
rhs: MatRef<'_, Standard<half::f16>>,
scratch: &mut [f32],
arch: Scalar,
query: BlockTransposedRef<'_, half::f16, SC_A>,
docs: MatRef<'_, Standard<half::f16>>,
state: &mut [f32],
) {
max_ip_kernel(arch, lhs, rhs, scratch, TileBudget::default());
float::run(
arch,
query.padded_nrows(),
docs.num_vectors(),
query.padded_ncols(),
TileBudget::default(),
state,
|plan: Plan<SC_A, SC_B>| {
(
QueryWiden::new(arch, query, plan.a_panels),
DocWiden::new(arch, docs, plan.b_panels),
)
},
);
}
}
Loading
Loading