makepad/libs/ai/models/common/src/backend.rs
Admin 60615db4ed libs: ai hub/models/cuda, speech, chat_ui
Squash of 54 work commits (Sep 1–12):
  6251f7c  ai-hub: body domain — live pose packets ride the realtime session
  ea50c77  chat_ui: the feed's session gets its profile brief back
  f51b5f3  ai-body: the crate for the native SAM 3D Body port, with its weights reader
  8211ae6  ai-body: the MHR rig and the pose head's parameter decoding, oracle-exact
  9e343a8  ai-body: the DINOv3 ViT-H+/16 backbone, crop and ray conditioning; Metal gains rope-half and affine layer norm
  69d842c  ai-body: the promptable pose decoder and its refinement loop, oracle-matched on Metal
  66e5e2f  ai-hub: SAM 3D Body runs natively — `sam3dbody` on the body domain, oracle-matched end to end
  a634198  ai-hub: the body-native commit carried a peer's in-flight hub hunks; put them back where they were
  9ff44e8  ai-hub: the body-native wiring, this time only the lane's hunks
  6a1c16b  ai-body: third-party notices — what the port is implemented after, and what it is not
  d78411a  ai-body: the per-step work moves to the GPU
  b22259b  ai-body: the context stays on the GPU; only the pose token leaves the loop
  346f31f  ai-body: flash attention for the head-dim-64 blocks
  45b5b98  ai-body: the crop size is a runtime knob, and the loop reports where its time goes
  4be6d19  ai-body: the test modules import the grid constants they still use
  7598346  ai-body: tensor-core GEMMs for the backbone, and the rig's correctives only where they count
  a9ce596  ai-body: the crop warp runs across cores
  8964ba6  ai-body: an FP8 backbone mode, off by default, measured against the oracle
  a2aaa8f  ai-body: the FP8 bias rides a column-broadcast add on the device
  d53c77d  metal: a device-resident ViT stack, and the body backbone rides it
  d006d0a  metal: resident f32 linears keep their weight on the device
  525ba1c  metal: a device-resident two-way decoder layer, and the body decoder rides it
  c9e6d88  ai-body: the hands pass — hand crops, the hand decoder, the hand-mode rig and the wrist fusion
  62dff26  ai-body: the mask prompt — a person's segmentation mask conditions the body pass
  a648cf8  ai-hub: body session options — hands, detect, persons=N
  8c568df  ai-hub: drop the SAM 3D Body reference worker backend
  7ff875a  ai-hub: keep a peer's in-flight beats/notes/local work out of the body commits
  31e5faa  ai-hub: local model runner, licence acknowledgements, a shared install panel; Beat This!, Basic Pitch and the Salamander drum-kit entries
  b94bc58  ai-services: the wire, the app port and the panel state — one conversation, many apps
  2acb798  ai-services: wire v2 — endpoints, receiver-side caps, result disposition
  8ae0ffb  ai-services: the engine core — registry, router and conversation, tested against a scripted model
  2308736  ai-services: the real models behind the engine feature — local through the hub, Claude, and none
  c3f631d  livepipe: one reusable pipe from a camera to a fleet node and back
  ff62db3  ai libs: the runtime env-var cleanup — precision is a per-caller policy, not an environment side channel
  04a94ef  realtime: one service-log line when a live session opens and one when it closes
  0ecb81c  ai models: the model-crates env-var cleanup — 172 research knobs gone, the unset default is the code
  4ca36c1  ai hub + services: the assistant's model comes from wherever it is resident — the fleet chat box, with tools, then the local weights
  432121e  aichat engine + wm: launch, then use — the assistant continues in the same turn once the app it started is on the bus
  7a5bf69  ai-hub registry: the Salamander drumkit samples come from the makepad.nl mirror — the GitHub repo only carries the .sfz files
  102ffc5  ai-services: messages on the bus — a manifest declares topics, the engine subscribes on a tool's behalf or by ToolResult.subscribe, a service publishes Message frames, an idle conversation wakes on a message as an event turn under rate laws; the WM bus forwards the new frames; every app that matches the wire gets its arm
  a837792  hub + flow: a whitespace-only chat completion is retried once and then fails instead of passing as an answer; a flow's model is a fleet model id unless it names a weight file on disk; chat models show under the text domain in /v1/models
  bc6c620  hub + flow: what the chat review found — the in-process route retries an empty completion too, a node says whether its prefill opened thinking so a brief-mode answer is never discarded, a preferred model falls back to normal election when no node has it, discovery keeps looking for the preferred model until patience runs out
  75c3441  hub: the PRO 6000 serves image as well as chat and text
  ad5e98b  hub registry: flux2-dev's VRAM estimate is its measured peak, 30 GB
  c7241e0  hub: a node that evicted every resident releases its cached allocator pool before refusing a load or publishing usable VRAM
  30575f0  flow: route generation by request workload
  1be1e21  ai-hub: gate downloads by disk capacity and recover fleet admission
  df6b394  filesystem_watcher, bounded_http, ai services: live and tool prerequisites
  79ebdb9  ai-hub: add a native Pixal3D image-to-3D backend
  0ba0d74  ai-hub: propagate typed refusals under reject queue policy
  cc6c872  Speed up H3 conditioning and video decoding
  e512059  Fix Qwen vision residency and generated material colors
  2864f68  ai-hub http client: bound every plain TCP connect to 3 s per address
  3d93229  ai: CUDA is a Linux/Windows-only dependency; the hub library defaults to llm + stt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 13:40:31 +02:00

340 lines
14 KiB
Rust

use crate::{DiffusionError, Result};
use makepad_ai_llm::metal_compiled::{self as backend_impl, MetalGraphSession, MetalPreparedGraph};
use makepad_ai_llm::{Context, CudaExecRuntime, CudaRawGraphSession, Graph};
use std::collections::BTreeMap;
pub use crate::accel::*;
pub use crate::gpu as cuda;
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub use makepad_ai_cuda::llm_ops;
pub use makepad_ai_loader::prof;
pub use makepad_ai_metal::{BackendCapabilities, BackendInfo, BackendKind};
pub mod metal {
pub use makepad_ai_llm::metal_compiled::*;
pub use makepad_ai_metal::*;
}
pub use metal::{
try_add_f32, try_attention_softmax_weighted_sum_f32, try_conv2d_planar_f32,
try_flash_attn_f32_packed, try_gelu_f32, try_group_norm_planar_f32,
try_layer_norm_mul_add_f32, try_matmul_nn_f32, try_matmul_nt_f32, try_mul_f32,
try_rms_norm_mul_f32, try_silu_f32, BufferStorageMode,
MetalGraphTensorWrite as GraphTensorWrite, MetalRuntime as Runtime,
};
/// Device-resident tensor API (CUDA today; Metal-backed stub elsewhere).
/// Activations stay on the GPU across a whole transformer step — see the
/// flux device path in flux_transformer.rs.
pub use crate::gpu::{
gpu_act_f16_enabled, gpu_add, gpu_add_bf16, gpu_add_cols_broadcast, gpu_alias_snake_updown2x,
gpu_attention_cross_fused_enabled,
gpu_attention_gqa_decode_bf16, gpu_attention_gqa_decode_pair_bf16, gpu_attention_packed,
gpu_attention_packed_f32,
gpu_attention_packed_bf16,
gpu_attention_packed_causal, gpu_attention_packed_causal_bf16,
gpu_attention_packed_causal_f16, gpu_attention_packed_causal_f32,
gpu_attention_packed_causal_flash,
gpu_attention_packed_flash_cross, gpu_attention_packed_flash_cross_bf16_rn,
gpu_attention_packed_flash_cross_bf16pre_f16,
gpu_attention_packed_composite_bf16, gpu_attention_packed_composite_f32,
gpu_attention_packed_motion_text,
gpu_attention_packed_cross, gpu_attention_packed_cross_bias, gpu_rpb_expand, gpu_sam3_sine_embed, gpu_sam3_rpb_axial, gpu_sam3_refine_boxes, gpu_attention_packed_cross_bf16,
gpu_attention_packed_cross_composite_bf16, gpu_gather_rows_colblock,
gpu_attention_packed_flash_bf16, gpu_attention_packed_flash2_d64,
gpu_gelu_erf, gpu_rms_norm_mul_perhead,
gpu_attention_planar_single,
gpu_birefnet_broadcast, gpu_birefnet_deform_conv2d_cached,
gpu_birefnet_global_avg_pool, gpu_birefnet_image_to_patches,
gpu_birefnet_mul_sigmoid_mask, gpu_birefnet_relu, gpu_birefnet_resize_bilinear,
gpu_birefnet_swin_attention, gpu_birefnet_tokens_to_planar,
gpu_beam_cache_reorder_append, gpu_bf16_round, gpu_concat_cols, gpu_concat_rows,
gpu_concat_rows_many,
gpu_conv2d_planar_cached, gpu_conv2d_planar_strided, gpu_copy_into, gpu_device_available, gpu_download, gpu_gather_cols,
gpu_gated_residual,
gpu_gated_residual_mod, gpu_gated_residual_mod_round_bf16, gpu_gelu, gpu_gelu_bias_f16,
gpu_graph_capture, gpu_graph_launch,
gpu_group_norm_planar,
gpu_layer_norm_mod, gpu_layer_norm_mod_f16,
gpu_gated_residual_indexed, gpu_gemm_f16acc_enabled,
gpu_layer_norm_mul_add, gpu_layer_norm_mul_add_grouped, gpu_layer_norm_pytorch,
gpu_linear_f32_resident, gpu_mul,
gpu_layer_norm_mod_to_bf16buf, gpu_bf16buf_slab_to_f32, gpu_rms_norm_mul_from_bf16_slab,
gpu_swiglu_gate_first_from_bf16, gpu_concat_f32rn_bf16buf,
gpu_linear_nt_cached, gpu_linear_nt_cached_with_precision,
gpu_linear_nt_cached_f16_with_precision, gpu_linear_nt_cached_bf16_bias_epilogue,
gpu_linear_nt_cached_bf16_f32acc, gpu_linear_nt_cached_bf16_mm,
gpu_linear_nt_cached_bf16_mm_from_buf, gpu_linear_nt_cached_bf16_mm_from_buf_to_buf,
gpu_linear_nt_cached_f8_mm, gpu_linear_nt_cached_f8_mm_from_buf,
gpu_linear_nt_cached_f8_mm_from_buf_to_buf,
gpu_stream_ring_active, gpu_stream_ring_advance, gpu_stream_ring_prime,
gpu_stream_ring_release_slots, gpu_stream_ring_setup,
gpu_linear_nt_cached_f16_f32acc,
gpu_linear_nt_cached_f16,
gpu_perf_stats, gpu_pool_cap_override, gpu_pool_clear, gpu_pool_trim,
gpu_rms_norm_mod_indexed, gpu_rms_norm_mul, gpu_rms_norm_mul_bf16, gpu_rms_norm_qwen3,
gpu_sparse_conv27,
gpu_skintokens_michelangelo_fourier,
gpu_pixel_shuffle_planar, gpu_pixel_shuffle_planar_cached, gpu_reshape,
gpu_realesrgan_alloc_f16, gpu_realesrgan_alloc_f32,
gpu_realesrgan_bias_lrelu_f16,
gpu_realesrgan_bias_lrelu_f32, gpu_realesrgan_conv3x3_f16, gpu_realesrgan_conv3x3_f32,
gpu_realesrgan_lrelu, gpu_realesrgan_quantize_rgb8_f32,
gpu_realesrgan_scale_add, gpu_realesrgan_spine_axpb,
gpu_rife_conv_transpose2d, gpu_rife_fill, gpu_rife_merge_rgb8, gpu_rife_res_conv,
gpu_rife_scale, gpu_rife_warp,
gpu_rope_half, gpu_rope_half_bf16, gpu_rope_interleaved, gpu_silu, gpu_slice_cols, gpu_slice_rows,
gpu_vit_backbone_resident, GpuVitLayer, GpuVitLinear,
gpu_two_way_layer_resident, GpuTwoWayAttention, GpuTwoWayLayer, GpuTwoWayLinear,
gpu_splat_repo3d_tables, gpu_splat_rope_pairs_per_head,
gpu_swiglu_gate_first, gpu_swiglu_value_gate, gpu_to_f16, gpu_upload, gpu_wavenet_gate,
gpu_quant_linear_type_supported,
gpu_release_cached, gpu_runtime_trim, gpu_upload_into, gpu_upload_u32, gpu_weight_cache_ensure,
gpu_weight_cache_ensure_quant,
gpu_weight_cache_evict_prefix, gpu_weight_cache_evict_prefix_if_loaded,
gpu_weight_cache_protect_prefixes,
gpu_upsample_nearest2x, GemmPrecision, GpuBf16Buf, GpuLinearPart, GpuPerfStats, GpuStepGraph, GpuTensor,
};
pub type GraphSession = MetalGraphSession;
pub type PreparedGraph = MetalPreparedGraph;
/// Import backend primitives through this module so diffusion stays agnostic
/// about whether ggml is driving the compiled graph through Metal, CUDA, or a
/// future backend-specific implementation detail.
pub fn new_runtime() -> Result<Runtime> {
Runtime::new().map_err(DiffusionError::model)
}
pub fn runtime_available() -> bool {
Runtime::is_available()
}
pub fn prepare_graph(runtime: &Runtime, ctx: &Context, graph: &Graph) -> Result<PreparedGraph> {
backend_impl::prepare_graph(ctx, graph, runtime.features()).map_err(DiffusionError::model)
}
pub fn create_graph_session(
runtime: &Runtime,
ctx: &Context,
prepared: &PreparedGraph,
input_storage: BufferStorageMode,
output_storage: BufferStorageMode,
) -> Result<GraphSession> {
GraphSession::from_runtime(
runtime.clone(),
ctx,
prepared,
input_storage,
output_storage,
)
.map_err(DiffusionError::model)
}
pub fn compile_graph_session(
runtime: &Runtime,
ctx: &Context,
graph: &Graph,
input_storage: BufferStorageMode,
output_storage: BufferStorageMode,
) -> Result<GraphSession> {
let prepared = prepare_graph(runtime, ctx, graph)?;
create_graph_session(runtime, ctx, &prepared, input_storage, output_storage)
}
// ---------------------------------------------------------------------------
// Device-neutral compiled-graph seam
//
// The `Runtime` / `GraphSession` pair above is Metal-typed: it is what the
// flux text encoders and VAE were written against, and on a CUDA box those
// models run through the imperative `gpu_*` surface instead. A model whose
// whole forward pass is ONE ggml graph (BS-RoFormer / stems) wants the third
// thing — the same graph, either store — so it gets this pair, which picks a
// device at runtime and fails closed with both reasons if neither is usable.
// ---------------------------------------------------------------------------
/// Which compiled-graph store a [`DeviceRuntime`] is driving.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GraphDevice {
Metal,
Cuda,
}
impl GraphDevice {
pub fn name(self) -> &'static str {
match self {
GraphDevice::Metal => "metal",
GraphDevice::Cuda => "cuda",
}
}
}
/// A device that can compile and run a ggml `Graph`.
pub enum DeviceRuntime {
Metal(Runtime),
Cuda(CudaExecRuntime),
}
/// One graph compiled for a [`DeviceRuntime`].
pub enum DeviceGraphSession {
Metal(GraphSession),
Cuda(CudaRawGraphSession),
}
/// What one execution produced, keyed by the output `TensorId` the caller
/// asked for.
pub struct GraphExecution {
pub outputs: BTreeMap<makepad_ai_llm::TensorId, Vec<u8>>,
}
/// `MAKEPAD_AI_GRAPH_BACKEND=metal|cuda` pins the choice; otherwise CUDA wins
/// where it exists (it is the fleet path and an order of magnitude faster)
/// and Metal is the fallback.
fn requested_graph_device() -> Option<GraphDevice> {
match std::env::var("MAKEPAD_AI_GRAPH_BACKEND")
.ok()
.as_deref()
.map(str::trim)
{
Some("cuda") | Some("CUDA") => Some(GraphDevice::Cuda),
Some("metal") | Some("METAL") => Some(GraphDevice::Metal),
_ => None,
}
}
impl DeviceRuntime {
/// Bind a device: the pinned one if `MAKEPAD_AI_GRAPH_BACKEND` names it,
/// else CUDA, else Metal. An explicit request that cannot be honoured is
/// an error rather than a silent fallback — a fleet job that quietly ran
/// on the wrong store would be worse than one that refused to start.
pub fn new() -> Result<Self> {
match requested_graph_device() {
Some(GraphDevice::Cuda) => CudaExecRuntime::new()
.map(DeviceRuntime::Cuda)
.map_err(|err| {
DiffusionError::model(format!(
"MAKEPAD_AI_GRAPH_BACKEND=cuda but CUDA is unusable: {err}"
))
}),
Some(GraphDevice::Metal) => Runtime::new().map(DeviceRuntime::Metal).map_err(|err| {
DiffusionError::model(format!(
"MAKEPAD_AI_GRAPH_BACKEND=metal but Metal is unusable: {err}"
))
}),
None => match CudaExecRuntime::new() {
Ok(runtime) => Ok(DeviceRuntime::Cuda(runtime)),
Err(cuda_error) => match Runtime::new() {
Ok(runtime) => Ok(DeviceRuntime::Metal(runtime)),
Err(metal_error) => Err(DiffusionError::model(format!(
"no compiled-graph device available (cuda: {cuda_error}; metal: \
{metal_error})"
))),
},
},
}
}
pub fn device(&self) -> GraphDevice {
match self {
DeviceRuntime::Metal(_) => GraphDevice::Metal,
DeviceRuntime::Cuda(_) => GraphDevice::Cuda,
}
}
pub fn description(&self) -> String {
match self {
DeviceRuntime::Metal(runtime) => format!("metal:{}", runtime.backend_info().name),
DeviceRuntime::Cuda(runtime) => runtime.device_description(),
}
}
/// Compile `graph` over `ctx`. `outputs` are the tensors the caller will
/// read back; they are pinned alive so the activation planner cannot
/// recycle their storage mid-graph.
pub fn compile_graph(
&self,
ctx: &Context,
graph: &Graph,
outputs: &[makepad_ai_llm::TensorId],
input_storage: BufferStorageMode,
output_storage: BufferStorageMode,
) -> Result<DeviceGraphSession> {
match self {
DeviceRuntime::Metal(runtime) => Ok(DeviceGraphSession::Metal(compile_graph_session(
runtime,
ctx,
graph,
input_storage,
output_storage,
)?)),
DeviceRuntime::Cuda(runtime) => Ok(DeviceGraphSession::Cuda(
runtime
.create_raw_graph_session(ctx, graph, outputs)
.map_err(|err| DiffusionError::model(err.to_string()))?,
)),
}
}
}
impl DeviceGraphSession {
pub fn device(&self) -> GraphDevice {
match self {
DeviceGraphSession::Metal(_) => GraphDevice::Metal,
DeviceGraphSession::Cuda(_) => GraphDevice::Cuda,
}
}
pub fn execute(
&self,
ctx: &Context,
writes: &[(makepad_ai_llm::TensorId, &[u8])],
outputs: &[makepad_ai_llm::TensorId],
) -> Result<GraphExecution> {
match self {
DeviceGraphSession::Metal(session) => {
let inputs: Vec<GraphTensorWrite<'_>> = writes
.iter()
.map(|(tensor_id, bytes)| GraphTensorWrite {
tensor_id: *tensor_id,
bytes,
})
.collect();
let run = session
.execute(ctx, &inputs, outputs)
.map_err(DiffusionError::model)?;
Ok(GraphExecution {
outputs: run.outputs,
})
}
DeviceGraphSession::Cuda(session) => Ok(GraphExecution {
outputs: session
.execute(ctx, writes, outputs)
.map_err(|err| DiffusionError::model(err.to_string()))?,
}),
}
}
}
/// Release model-owned dense CUDA weight namespaces plus every reusable
/// activation/scratch buffer on the current thread. Teardown is deliberately
/// conditional: a cold/CPU backend never initializes CUDA just to unload.
/// All prefixes are attempted before an error is returned so one bad release
/// cannot strand the remaining model namespaces.
pub fn release_gpu_runtime_namespaces(prefixes: &[&str]) -> Result<usize> {
let mut released = 0usize;
let mut errors = Vec::new();
for prefix in prefixes {
match gpu_weight_cache_evict_prefix_if_loaded(prefix) {
Ok(count) => released += count,
Err(error) => errors.push(format!("evict {prefix:?}: {error}")),
}
}
if let Err(error) = gpu_runtime_trim() {
errors.push(format!("trim CUDA scratch: {error}"));
}
if errors.is_empty() {
Ok(released)
} else {
Err(DiffusionError::model(errors.join("; ")))
}
}