virtio-accel 0.1.0

Portable Rust foundations for a virtio accelerator device
Documentation
use std::cell::Cell;
use std::collections::BTreeSet;
use std::mem::size_of;

use serde_json::Value;
use virtio_accel::core::{
    AcceleratorClass, BackendError, ByteSource, Capabilities, DeviceIdentity, DeviceInfo,
    DeviceLimits,
};
use virtio_accel::device::{DecodedRequestBody, DecoderLimits, FrameDecodeError, FrameDecoder};
use virtio_accel::proto::{
    BASELINE_COMMAND_QUEUES, HARD_MAX_BINDINGS, HARD_MAX_CHAIN_DESCRIPTORS, HARD_MAX_REQUEST_BYTES,
    HARD_MAX_RESPONSE_BYTES, KnownOpcode, Le16, Le32, LoadProgramRequest, PROTOCOL_MAJOR,
    PROTOCOL_MINOR, RequestHeader, StatusCode, SubmitRequest, WireConfig,
};

#[derive(Debug)]
struct CountingSource {
    bytes: Vec<u8>,
    read_bytes: Cell<u64>,
}

impl CountingSource {
    fn new(bytes: Vec<u8>) -> Self {
        Self {
            bytes,
            read_bytes: Cell::new(0),
        }
    }

    fn read_bytes(&self) -> u64 {
        self.read_bytes.get()
    }
}

impl ByteSource for CountingSource {
    fn len(&self) -> u64 {
        self.bytes.len() as u64
    }

    fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
        let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?;
        let end = start
            .checked_add(target.len())
            .filter(|end| *end <= self.bytes.len())
            .ok_or(BackendError::OutOfBounds)?;
        self.read_bytes
            .set(self.read_bytes.get() + u64::try_from(target.len()).unwrap());
        target.copy_from_slice(&self.bytes[start..end]);
        Ok(())
    }
}

fn decoder() -> FrameDecoder {
    let config = WireConfig {
        protocol_major: Le16::new(PROTOCOL_MAJOR),
        protocol_minor: Le16::new(PROTOCOL_MINOR),
        command_queue_count: Le16::new(BASELINE_COMMAND_QUEUES),
        max_chain_descriptors: Le16::new(HARD_MAX_CHAIN_DESCRIPTORS),
        max_request_bytes: Le32::new(HARD_MAX_REQUEST_BYTES),
        max_response_bytes: Le32::new(HARD_MAX_RESPONSE_BYTES),
    };
    let info = DeviceInfo {
        identity: DeviceIdentity {
            uuid: [0; 16],
            class: AcceleratorClass::NPU,
            vendor_id: 0,
            device_id: 0,
        },
        capabilities: Capabilities::HOST_VISIBLE_MEMORY
            | Capabilities::DEVICE_LOCAL_MEMORY
            | Capabilities::SHARED_MEMORY,
        limits: DeviceLimits {
            max_contexts: 64,
            max_buffers_per_context: 1024,
            max_programs_per_context: 256,
            max_queues_per_context: 16,
            max_events_per_context: 4096,
            max_bindings_per_submission: HARD_MAX_BINDINGS,
            max_buffer_bytes: 1 << 30,
            max_artifact_bytes: 1 << 30,
        },
    };
    FrameDecoder::new(DecoderLimits::new(&config, info).unwrap())
}

fn scenario_request(scenario_name: &str, request_id: u64) -> Vec<u8> {
    let corpus: Value =
        serde_json::from_str(include_str!("../conformance/v1.0/scenarios.json")).unwrap();
    let scenario = corpus["scenarios"]
        .as_array()
        .unwrap()
        .iter()
        .find(|scenario| scenario["name"] == scenario_name)
        .unwrap();
    let request = scenario["trace"]
        .as_array()
        .unwrap()
        .iter()
        .find(|entry| entry["request_id"].as_u64() == Some(request_id))
        .unwrap()["request"]
        .as_str()
        .unwrap();
    decode_hex(request)
}

fn decode_hex(hex: &str) -> Vec<u8> {
    hex.as_bytes()
        .chunks_exact(2)
        .map(|pair| {
            let high = (pair[0] as char).to_digit(16).unwrap() as u8;
            let low = (pair[1] as char).to_digit(16).unwrap() as u8;
            (high << 4) | low
        })
        .collect()
}

#[test]
fn performance_budget_manifest_covers_every_v1_hot_path() {
    let budgets: Value =
        serde_json::from_str(include_str!("../conformance/v1.0/performance-budgets.json")).unwrap();
    assert_eq!(budgets["schema"], "virtio-accel-performance-budgets-1");
    let operations = budgets["operations"].as_array().unwrap();
    let ids = operations
        .iter()
        .map(|entry| entry["id"].as_str().unwrap())
        .collect::<BTreeSet<_>>();
    assert_eq!(
        ids,
        BTreeSet::from([
            "wire.config_decode",
            "wire.request_decode_non_submit",
            "wire.submit_decode",
            "transport.segmented_region_access",
            "state.object_lookup",
            "device.command_dispatch",
            "device.submission_admission",
            "device.polling",
            "device.reset",
            "provider.explicit_transfer",
            "provider.copy_path_diagnostics",
        ])
    );
    for entry in operations {
        assert_eq!(entry["allocates_from_unvalidated_guest_count"], false);
        assert!(entry["complexity"].as_str().unwrap().starts_with("O("));
        assert!(entry["allocation_profile"].is_string());
        assert!(entry["copy_profile"].is_string());
        assert!(entry["permitted_copy_boundary"].is_string());
        assert!(entry["thresholds"].is_object());
    }
}

#[test]
fn baseline_metadata_tracks_the_checked_in_budget_manifest() {
    let baseline: Value = serde_json::from_str(include_str!(
        "../conformance/v1.0/performance-baseline.json"
    ))
    .unwrap();
    assert_eq!(baseline["schema"], "virtio-accel-performance-baseline-1");
    assert_eq!(baseline["budgets"], "performance-budgets.json");
    assert_eq!(baseline["protocol"], "1.0");
    assert!(baseline["toolchain"]["msrv"].is_string());
    assert_eq!(baseline["host"]["hardware_acceleration"], "none");
    assert!(
        baseline["deterministic_results"]
            .as_array()
            .unwrap()
            .iter()
            .all(
                |result| result["value"].as_u64().unwrap() <= result["threshold"].as_u64().unwrap()
            )
    );
}

#[test]
fn non_submit_decode_does_not_read_or_stage_bulk_artifact_tail() {
    let request = scenario_request("complete_lifecycle", 6);
    let source = CountingSource::new(request);
    let decoded = decoder().decode(&source, 24).unwrap();
    let DecodedRequestBody::LoadProgram { payload, .. } = decoded.body() else {
        panic!("scenario request 6 is not LOAD_PROGRAM");
    };
    assert_eq!(decoded.body().opcode(), KnownOpcode::LoadProgram);
    assert_eq!(payload.len(), 24);
    assert_eq!(
        source.read_bytes(),
        u64::try_from(size_of::<RequestHeader>() + size_of::<LoadProgramRequest>()).unwrap()
    );
}

#[test]
fn submit_rejects_unvalidated_binding_count_before_reading_binding_tail() {
    let mut request = scenario_request("complete_lifecycle", 8);
    let binding_count_offset = size_of::<RequestHeader>() + 16;
    request[binding_count_offset..binding_count_offset + 4]
        .copy_from_slice(&(HARD_MAX_BINDINGS + 1).to_le_bytes());
    let source = CountingSource::new(request);

    assert!(matches!(
        decoder().decode(&source, 24),
        Err(FrameDecodeError::Protocol {
            status: StatusCode::RESOURCE_LIMIT,
            ..
        })
    ));
    assert_eq!(
        source.read_bytes(),
        u64::try_from(size_of::<RequestHeader>() + size_of::<SubmitRequest>()).unwrap()
    );
}