weirflow 0.1.0

GPU-first dataflow analysis primitives for Vyre and Santh compiler pipelines.
Documentation
use super::*;
use crate::fixed_point_resident_frontier::FixedPointResidentFrontierScratch;
use std::{
    collections::HashSet,
    sync::{
        atomic::{AtomicU64, Ordering},
        Arc, Mutex,
    },
};
use vyre::backend::{private, BackendError, DispatchConfig, OutputBuffers, Resource};

struct FakeResidentBackend {
    next: AtomicU64,
    uploads: Mutex<Vec<(u64, usize)>>,
    freed: Mutex<Vec<u64>>,
    supported_ops: HashSet<vyre::ir::OpId>,
}

impl FakeResidentBackend {
    fn new() -> Self {
        Self {
            next: AtomicU64::new(1),
            uploads: Mutex::new(Vec::new()),
            freed: Mutex::new(Vec::new()),
            supported_ops: HashSet::new(),
        }
    }
}

impl private::Sealed for FakeResidentBackend {}

impl vyre::VyreBackend for FakeResidentBackend {
    fn id(&self) -> &'static str {
        "weir_test_fake_borrowed_resident_plan"
    }

    fn supported_ops(&self) -> &HashSet<vyre::ir::OpId> {
        &self.supported_ops
    }

    fn dispatch(
        &self,
        _program: &vyre::ir::Program,
        _inputs: &[Vec<u8>],
        _config: &DispatchConfig,
    ) -> Result<Vec<Vec<u8>>, BackendError> {
        unreachable!("borrowed resident plan tests do not dispatch through host bytes")
    }

    fn allocate_resident(&self, _byte_len: usize) -> Result<Resource, BackendError> {
        Ok(Resource::Resident(
            self.next.fetch_add(1, Ordering::Relaxed),
        ))
    }

    fn upload_resident(&self, resource: &Resource, bytes: &[u8]) -> Result<(), BackendError> {
        let Resource::Resident(handle) = resource else {
            panic!("fake backend only uploads resident resources");
        };
        self.uploads.lock().unwrap().push((*handle, bytes.len()));
        Ok(())
    }

    fn upload_resident_many(&self, uploads: &[(&Resource, &[u8])]) -> Result<(), BackendError> {
        for &(resource, bytes) in uploads {
            self.upload_resident(resource, bytes)?;
        }
        Ok(())
    }

    fn upload_resident_at_many(
        &self,
        uploads: &[(&Resource, usize, &[u8])],
    ) -> Result<(), BackendError> {
        for &(resource, offset, bytes) in uploads {
            assert_eq!(
                offset, 0,
                "borrowed resident plan test backend only supports prefix uploads"
            );
            self.upload_resident(resource, bytes)?;
        }
        Ok(())
    }

    fn free_resident(&self, resource: Resource) -> Result<(), BackendError> {
        let Resource::Resident(handle) = resource else {
            panic!("fake backend only frees resident resources");
        };
        self.freed.lock().unwrap().push(handle);
        Ok(())
    }
}

struct FakeResidentPipeline {
    seen_resident_prefix: Mutex<Vec<usize>>,
}

impl FakeResidentPipeline {
    fn new() -> Self {
        Self {
            seen_resident_prefix: Mutex::new(Vec::new()),
        }
    }
}

impl private::Sealed for FakeResidentPipeline {}

impl vyre::CompiledPipeline for FakeResidentPipeline {
    fn id(&self) -> &str {
        "weir_test_fake_borrowed_resident_pipeline"
    }

    fn dispatch(
        &self,
        _inputs: &[Vec<u8>],
        _config: &DispatchConfig,
    ) -> Result<Vec<Vec<u8>>, BackendError> {
        unreachable!("borrowed resident plan tests do not dispatch through host bytes")
    }

    fn dispatch_persistent_handles(
        &self,
        inputs: &[Resource],
        _config: &DispatchConfig,
    ) -> Result<OutputBuffers, BackendError> {
        let resident_prefix = inputs
            .iter()
            .take_while(|resource| matches!(resource, Resource::Resident(_)))
            .count();
        self.seen_resident_prefix
            .lock()
            .unwrap()
            .push(resident_prefix);
        Ok(vec![vec![1, 0, 0, 0]])
    }
}

#[test]
fn borrowed_resident_plan_uses_cached_graph_without_owning_it() {
    let backend = FakeResidentBackend::new();
    let graph = crate::reaching::prepare_reaching_graph(
        2,
        &[0, 1, 1],
        &[1],
        &[vyre_primitives::predicate::edge_kind::CONTROL],
    )
    .expect("valid graph must pack");
    let mut cache = crate::fixed_point_resident_cache::FixedPointResidentGraphCache::new();
    let resident = cache
        .get_or_upload(&backend, &graph)
        .expect("cache must upload graph once");
    let pipeline = Arc::new(FakeResidentPipeline::new());
    let plan = FixedPointBorrowedResidentPlan::new(resident, pipeline.clone());
    let mut host_scratch = crate::fixed_point_scratch::FixedPointScratch::default();
    let mut resident_frontier = FixedPointResidentFrontierScratch::default();

    let result = plan
        .reaching_reusing_frontier(
            &backend,
            &[0b01],
            1,
            &DispatchConfig::default(),
            &mut host_scratch,
            &mut resident_frontier,
        )
        .expect("borrowed resident plan must dispatch through cached graph");

    assert_eq!(result, vec![0b01]);
    assert_eq!(plan.node_count(), 2);
    assert_eq!(plan.edge_count(), 1);
    assert_eq!(plan.resident_resource_count(), 5);
    assert_eq!(
            backend.uploads.lock().unwrap().len(),
            6,
            "borrowed resident plan must upload four unique cached graph buffers plus frontier_in and frontier_out seed buffers"
        );
    assert_eq!(
        pipeline.seen_resident_prefix.lock().unwrap().as_slice(),
        &[7]
    );
    plan.clear_frontier(&backend, &mut resident_frontier)
        .expect("borrowed plan must free caller frontier resources");
    assert_eq!(
        backend.freed.lock().unwrap().len(),
        2,
        "clearing a borrowed plan frontier must not free cached graph resources"
    );
    cache
        .free_all(&backend)
        .expect("cache must free resident graph resources");
    assert_eq!(backend.freed.lock().unwrap().len(), 6);
}

#[test]
fn borrowed_sequence_window_rejects_plan_that_does_not_match_resident_graph() {
    let backend = FakeResidentBackend::new();
    let resident_graph = crate::reaching::prepare_reaching_plan(
        2,
        &[0, 1, 1],
        &[1],
        &[vyre_primitives::predicate::edge_kind::CONTROL],
    )
    .expect("resident plan fixture must pack");
    let mismatched_plan = crate::reaching::prepare_reaching_plan(
        3,
        &[0, 1, 2, 2],
        &[1, 2],
        &[vyre_primitives::predicate::edge_kind::CONTROL; 2],
    )
    .expect("mismatched plan fixture must pack");
    let resident = crate::fixed_point_resident::FixedPointResidentGraph::upload(
        &backend,
        resident_graph.graph(),
    )
    .expect("fake backend must upload resident graph");
    let borrowed =
        FixedPointBorrowedResidentPlan::new(&resident, Arc::new(FakeResidentPipeline::new()));
    let mut host_scratch = crate::fixed_point_scratch::FixedPointScratch::default();
    let mut resident_frontier = FixedPointResidentFrontierScratch::default();

    let error = borrowed
        .reaching_reusing_frontier_sequence_window(
            &backend,
            &mismatched_plan,
            &[0b001],
            4,
            &mut host_scratch,
            &mut resident_frontier,
        )
        .expect_err("sequence-window dispatch must reject mismatched prepared graph layout");

    assert!(
            error.contains("layout hash"),
            "Fix: sequence-window resident plan mismatch errors must name the graph layout hash; got {error}"
        );
    assert!(
        !resident_frontier.is_allocated(),
        "Fix: plan/layout mismatch must fail before allocating resident frontier scratch."
    );
    resident
        .free(&backend)
        .expect("resident graph resources must free after mismatch test");
}