Skip to content

Commit d276d16

Browse files
committed
Wiring and making GPU accel. optional with an argument
1 parent 317201a commit d276d16

4 files changed

Lines changed: 183 additions & 80 deletions

File tree

‎src/cli.rs‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ pub enum Commands {
197197
/// Calculate pairwise distances using sketches
198198
Dist {
199199
/// The .skm file used as the reference
200-
#[arg(required = true)]
201-
ref_db: String,
200+
#[arg(required_unless_present = "list_gpus")]
201+
ref_db: Option<String>,
202202

203203
/// The .skm file used as the query (omit for ref v ref)
204204
#[arg(group = "query")]
@@ -240,6 +240,17 @@ pub enum Commands {
240240
/// minimum completeness for a sample to be corrected but the completeness correction
241241
#[arg(long, default_value_t = 0.64)]
242242
completeness_cutoff: f64,
243+
244+
/// Calculate dense Jaccard/ANI distances on the GPU (needs a build with
245+
/// `--features wgpu`): alone, wgpu chooses, preferring a discrete GPU; with N, GPU N
246+
/// of --list-gpus. kNN and core/accessory distances stay on the CPU. Give it after
247+
/// the databases, or as --gpu=N, as it takes an optional value
248+
#[arg(long, num_args = 0..=1, value_name = "N")]
249+
gpu: Option<Option<usize>>,
250+
251+
/// List the GPUs --gpu N can choose from, and exit
252+
#[arg(long)]
253+
list_gpus: bool,
243254
},
244255

245256
/// Building and querying with inverted indices (.ski)

‎src/distances/mod.rs‎

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -232,22 +232,6 @@ fn self_dists_all_stream_generic<const BB: usize, W: IoWrite + Send>(
232232
completeness_cutoff: f64,
233233
threads: usize,
234234
) -> Result<(), Error> {
235-
#[cfg(feature = "wgpu")]
236-
{
237-
if matches!(dist_type, DistType::Jaccard(..)) {
238-
return dists_all_stream_wgpu::<BB, W>(
239-
writer,
240-
sketches,
241-
None,
242-
&dist_type,
243-
completeness_vec,
244-
None,
245-
completeness_cutoff,
246-
);
247-
}
248-
log::warn!("The wgpu backend does not support core/accessory distances yet; using the CPU");
249-
}
250-
251235
let ani = matches!(dist_type, DistType::Jaccard(_, _, true));
252236
let k_vals = match dist_type {
253237
DistType::Jaccard(k_idx, k_val, _) => Some((k_idx, k_val)),
@@ -333,22 +317,25 @@ fn self_dists_all_stream_generic<const BB: usize, W: IoWrite + Send>(
333317

334318

335319
/// GPU (wgpu) variant of [`self_dists_all_stream`] (`query_sketches` = None) and
336-
/// [`cross_dists_all_stream`], for Jaccard distances and ANI. Same line format; pairs come
337-
/// out tile by tile and chunk by chunk, in no global order, as in the CUDA code.
320+
/// [`cross_dists_all_stream`], for Jaccard distances and ANI, on GPU `gpu_device` of
321+
/// [`available_gpus`] (None: wgpu's choice). Same line format; pairs come out tile by tile
322+
/// and chunk by chunk, in no global order, as in the CUDA code.
338323
#[cfg(feature = "wgpu")]
339-
fn dists_all_stream_wgpu<const BB: usize, W: IoWrite + Send>(
324+
pub fn dists_all_stream_gpu<W: IoWrite + Send>(
340325
writer: &mut W,
341326
ref_sketches: &MultiSketch,
342327
query_sketches: Option<&MultiSketch>,
343-
dist_type: &DistType,
328+
dist_type: DistType,
344329
ref_completeness_vec: Option<&Vec<f64>>,
345330
query_completeness_vec: Option<&Vec<f64>>,
346331
completeness_cutoff: f64,
332+
gpu_device: Option<usize>,
347333
) -> Result<(), Error> {
348334
let pool_size = rayon::current_num_threads();
349335
if pool_size < 2 {
350336
bail!("GPU dense streaming requires a Rayon producer and writer; configure the pool with set_threads_with_writer()");
351337
}
338+
let bin_bits = if ref_sketches.is_legacy_format() { LEGACY_BIN_BITS } else { BIN_BITS };
352339
let ref_names = <DistanceMatrix as Distances>::sketch_names(ref_sketches);
353340
let query_names = query_sketches.map(<DistanceMatrix as Distances>::sketch_names);
354341

@@ -372,11 +359,12 @@ fn dists_all_stream_wgpu<const BB: usize, W: IoWrite + Send>(
372359
let gpu_result = pollster::block_on(get_distances_wgpu(
373360
ref_sketches,
374361
query_sketches,
375-
dist_type,
362+
&dist_type,
376363
ref_completeness_vec,
377364
query_completeness_vec,
378365
completeness_cutoff,
379-
BB,
366+
bin_bits,
367+
gpu_device,
380368
|tile, dists| match &query_names {
381369
// Self mode: both sides from the reference set, the lower index first
382370
None => tile.consume(dists, &ref_names, &ref_names, false, &text_tx),
@@ -391,6 +379,33 @@ fn dists_all_stream_wgpu<const BB: usize, W: IoWrite + Send>(
391379
})
392380
}
393381

382+
/// Stand-in for builds without the `wgpu` feature, so callers need no cfg of their own
383+
#[cfg(not(feature = "wgpu"))]
384+
pub fn dists_all_stream_gpu<W: IoWrite + Send>(
385+
_writer: &mut W,
386+
_ref_sketches: &MultiSketch,
387+
_query_sketches: Option<&MultiSketch>,
388+
_dist_type: DistType,
389+
_ref_completeness_vec: Option<&Vec<f64>>,
390+
_query_completeness_vec: Option<&Vec<f64>>,
391+
_completeness_cutoff: f64,
392+
_gpu_device: Option<usize>,
393+
) -> Result<(), Error> {
394+
bail!("This sketchlib was built without GPU support: rebuild it with --features wgpu")
395+
}
396+
397+
/// The GPUs [`dists_all_stream_gpu`] can use, one line each, `index: name (type, backend)`
398+
#[cfg(feature = "wgpu")]
399+
pub fn available_gpus() -> Result<Vec<String>, Error> {
400+
Ok(crate::gpu::dists_wgpu::gpu_list())
401+
}
402+
403+
/// Stand-in for builds without the `wgpu` feature
404+
#[cfg(not(feature = "wgpu"))]
405+
pub fn available_gpus() -> Result<Vec<String>, Error> {
406+
bail!("This sketchlib was built without GPU support: rebuild it with --features wgpu")
407+
}
408+
394409
/// Self query mode (dense, all distances)
395410
pub fn self_dists_knn<'a>(
396411
sketches: &'a MultiSketch,
@@ -709,22 +724,6 @@ fn cross_dists_all_stream_generic<const BB: usize, W: IoWrite + Send>(
709724
completeness_cutoff: f64,
710725
threads: usize,
711726
) -> Result<(), Error> {
712-
#[cfg(feature = "wgpu")]
713-
{
714-
if matches!(dist_type, DistType::Jaccard(..)) {
715-
return dists_all_stream_wgpu::<BB, W>(
716-
writer,
717-
ref_sketches,
718-
Some(query_sketches),
719-
&dist_type,
720-
ref_completeness_vec,
721-
query_completeness_vec,
722-
completeness_cutoff,
723-
);
724-
}
725-
log::warn!("The wgpu backend does not support core/accessory distances yet; using the CPU");
726-
}
727-
728727
let ani = matches!(dist_type, DistType::Jaccard(_, _, true));
729728
let k_vals = match dist_type {
730729
DistType::Jaccard(k_idx, k_val, _) => Some((k_idx, k_val)),

‎src/gpu/dists_wgpu.rs‎

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ pub async fn get_distances_wgpu(
187187
query_completeness_vec: Option<&Vec<f64>>, // only read with query_sketches
188188
completeness_cutoff: f64,
189189
bin_bits: usize, // BB chosen by the CPU dispatch: BIN_BITS, or LEGACY_BIN_BITS
190+
gpu_device: Option<usize>, // index in gpu_list(); None: wgpu's choice
190191
mut on_tile: impl FnMut(&DistTile, &[f32]) -> anyhow::Result<()>,
191192
) -> anyhow::Result<()> {
192193
let &DistType::Jaccard(k_idx, _, ani) = dist_type else {
@@ -211,22 +212,12 @@ pub async fn get_distances_wgpu(
211212
"Reference and query sketches differ in k-mer lengths, sketch size, hash type or format"
212213
);
213214

214-
// TODO: this could be a parameter? Also, we should expose which GPU to select by index, apart than from PowerPreference, which might be quite useful for browser but a bit less in highly optimised environments.
215-
let power_pref = wgpu::PowerPreference::None;
216-
let backends = wgpu::Backends::all();
217-
218-
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
219-
backends,
220-
// This fills the remaining items with the "..THING" and the thing itself.
221-
..wgpu::InstanceDescriptor::new_without_display_handle()
222-
});
223-
224-
let adapter = instance
225-
.request_adapter(&wgpu::RequestAdapterOptions {
226-
power_preference: power_pref, ..Default::default()
227-
})
228-
.await
229-
.context("No GPU adapter found")?;
215+
let instance = create_instance();
216+
let adapter = select_adapter(&instance, gpu_device).await?;
217+
let info = adapter.get_info();
218+
if info.device_type == wgpu::DeviceType::Cpu {
219+
log::warn!("{} is a software adapter, not a GPU: expect it to be slow", info.name);
220+
}
230221

231222
// Ask for the hardware limits rather than the WebGPU defaults, so the tile ranges and
232223
// workgroup sizes reflect what this device can actually do
@@ -239,7 +230,7 @@ pub async fn get_distances_wgpu(
239230
.await
240231
.context("Failed to get GPU device")?;
241232

242-
log::info!("Using this GPU: {}", adapter.get_info().name);
233+
log::info!("Using this GPU: {} ({:?}, {:?})", info.name, info.device_type, info.backend);
243234

244235
// =============================================
245236
// SETTING UP DATA!
@@ -670,6 +661,58 @@ pub async fn get_distances_wgpu(
670661
Ok(())
671662
}
672663

664+
// Native backends only: with OpenGL too, a GPU would be listed twice
665+
const GPU_BACKENDS: wgpu::Backends = wgpu::Backends::PRIMARY;
666+
667+
// The wgpu instance behind every GPU entry point
668+
fn create_instance() -> wgpu::Instance {
669+
wgpu::Instance::new(wgpu::InstanceDescriptor {
670+
backends: GPU_BACKENDS,
671+
// This fills the remaining items with the "..THING" and the thing itself.
672+
..wgpu::InstanceDescriptor::new_without_display_handle()
673+
})
674+
}
675+
676+
// The adapter --gpu N names, by its index in enumerate_adapters() (as --list-gpus shows
677+
// it); with --gpu alone, the adapter wgpu prefers for performance, a discrete GPU if any
678+
async fn select_adapter(
679+
instance: &wgpu::Instance,
680+
gpu_device: Option<usize>,
681+
) -> anyhow::Result<wgpu::Adapter> {
682+
match gpu_device {
683+
Some(index) => {
684+
let mut adapters = instance.enumerate_adapters(GPU_BACKENDS).await;
685+
ensure!(
686+
index < adapters.len(),
687+
"There is no GPU {index}: `sketchlib dist --list-gpus` lists the {} available",
688+
adapters.len()
689+
);
690+
Ok(adapters.swap_remove(index))
691+
}
692+
None => instance
693+
.request_adapter(&wgpu::RequestAdapterOptions {
694+
power_preference: wgpu::PowerPreference::HighPerformance,
695+
..Default::default()
696+
})
697+
.await
698+
.context("No GPU adapter found"),
699+
}
700+
}
701+
702+
/// The GPUs --gpu N chooses from, one line each, `index: name (type, backend)`, in the
703+
/// order it numbers them
704+
pub fn gpu_list() -> Vec<String> {
705+
let adapters = pollster::block_on(create_instance().enumerate_adapters(GPU_BACKENDS));
706+
adapters
707+
.iter()
708+
.enumerate()
709+
.map(|(index, adapter)| {
710+
let info = adapter.get_info();
711+
format!("{index}: {} ({:?}, {:?})", info.name, info.device_type, info.backend)
712+
})
713+
.collect()
714+
}
715+
673716
// Workgroups needed for all-vs-all: each query row of n - q - 1 references gets its
674717
// own workgroups, as in CUDA's getBlockSize()
675718
fn self_dispatch_count(n: u32, workgroup_size: u32) -> u64 {

‎src/lib.rs‎

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ use std::time::Instant;
154154
extern crate arrayref;
155155
extern crate num_cpus;
156156
use anyhow::{bail, Error};
157+
use crate::distances::distance_matrix::DistType;
157158
#[cfg(not(target_arch = "wasm32"))]
158159
use indicatif::ParallelProgressIterator;
159160
#[cfg(not(target_arch = "wasm32"))]
@@ -316,7 +317,19 @@ pub fn main() -> Result<(), Error> {
316317
ref_completeness_file,
317318
query_completeness_file,
318319
completeness_cutoff,
320+
gpu,
321+
list_gpus,
319322
} => {
323+
if *list_gpus {
324+
for gpu_line in available_gpus()? {
325+
println!("{gpu_line}");
326+
}
327+
return Ok(());
328+
}
329+
// clap requires it without --list-gpus
330+
let Some(ref_db) = ref_db.as_deref() else {
331+
bail!("dist needs a reference database");
332+
};
320333
if knn.is_none() {
321334
// Dense path streams output via an internal writer thread running
322335
// concurrently with compute (mirrors Sketch/Append/Inverted Build/Query).
@@ -353,6 +366,17 @@ pub fn main() -> Result<(), Error> {
353366
panic!("Error setting k size: {e}");
354367
});
355368

369+
// The GPU covers dense Jaccard and ANI distances; anything else stays on the CPU.
370+
// --gpu alone leaves the device to wgpu (gpu_device = None)
371+
if gpu.is_some() && !cfg!(feature = "wgpu") {
372+
bail!("This sketchlib was built without GPU support: rebuild it with --features wgpu");
373+
}
374+
let gpu_device = gpu.flatten();
375+
let use_gpu = gpu.is_some() && knn.is_none() && matches!(dist_type, DistType::Jaccard(..));
376+
if gpu.is_some() && !use_gpu {
377+
log::warn!("--gpu only covers dense Jaccard and ANI distances (-k without --knn) for now; using the CPU");
378+
}
379+
356380
// Read queries if supplied. Note no subsetting here
357381
let queries = if let Some(query_db_name) = query_db {
358382
log::info!("Loading query sketches from {query_db_name}.skm/.skd");
@@ -379,16 +403,29 @@ pub fn main() -> Result<(), Error> {
379403
// Self mode (dense)
380404
log::info!("Calculating all ref vs ref distances");
381405
log::info!("Streaming out in long matrix form");
382-
self_dists_all_stream(
383-
&mut output_file,
384-
&references,
385-
n,
386-
dist_type,
387-
args.quiet,
388-
ref_completeness_vec.as_ref(),
389-
*completeness_cutoff,
390-
*threads,
391-
)?;
406+
if use_gpu {
407+
dists_all_stream_gpu(
408+
&mut output_file,
409+
&references,
410+
None,
411+
dist_type,
412+
ref_completeness_vec.as_ref(),
413+
None,
414+
*completeness_cutoff,
415+
gpu_device,
416+
)?;
417+
} else {
418+
self_dists_all_stream(
419+
&mut output_file,
420+
&references,
421+
n,
422+
dist_type,
423+
args.quiet,
424+
ref_completeness_vec.as_ref(),
425+
*completeness_cutoff,
426+
*threads,
427+
)?;
428+
}
392429
}
393430
Some(mut nn) => {
394431
// Self mode (sparse): a genome cannot be its own neighbour
@@ -452,19 +489,32 @@ pub fn main() -> Result<(), Error> {
452489
// Cross-query mode (dense, all pairs)
453490
log::info!("Calculating all ref vs query distances");
454491
log::info!("Streaming out in long matrix form");
455-
cross_dists_all_stream(
456-
&mut output_file,
457-
&references,
458-
&query_db,
459-
n,
460-
n_query,
461-
dist_type,
462-
args.quiet,
463-
ref_completeness_vec.as_ref(),
464-
query_completeness_vec.as_ref(),
465-
*completeness_cutoff,
466-
*threads,
467-
)?;
492+
if use_gpu {
493+
dists_all_stream_gpu(
494+
&mut output_file,
495+
&references,
496+
Some(&query_db),
497+
dist_type,
498+
ref_completeness_vec.as_ref(),
499+
query_completeness_vec.as_ref(),
500+
*completeness_cutoff,
501+
gpu_device,
502+
)?;
503+
} else {
504+
cross_dists_all_stream(
505+
&mut output_file,
506+
&references,
507+
&query_db,
508+
n,
509+
n_query,
510+
dist_type,
511+
args.quiet,
512+
ref_completeness_vec.as_ref(),
513+
query_completeness_vec.as_ref(),
514+
*completeness_cutoff,
515+
*threads,
516+
)?;
517+
}
468518
}
469519
}
470520
}

0 commit comments

Comments
 (0)