mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Restructure GPU compilation execution pipeline (#903)
* Restructure gpu compilation execution pipeline * Add compilation server/client infrastructure * Add wgpu executor
This commit is contained in:
committed by
Keavon Chambers
parent
be32f7949f
commit
79ad3e7908
@@ -1,3 +0,0 @@
|
||||
pub mod compiler;
|
||||
pub mod context;
|
||||
pub mod executor;
|
||||
@@ -1,141 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::proto::*;
|
||||
use tera::Context;
|
||||
|
||||
fn create_cargo_toml(metadata: &Metadata) -> Result<String, tera::Error> {
|
||||
let mut tera = tera::Tera::default();
|
||||
tera.add_raw_template("cargo_toml", include_str!("templates/Cargo-template.toml"))?;
|
||||
let mut context = Context::new();
|
||||
context.insert("name", &metadata.name);
|
||||
context.insert("authors", &metadata.authors);
|
||||
context.insert("gcore_path", &format!("{}{}", env!("CARGO_MANIFEST_DIR"), "/../gcore"));
|
||||
tera.render("cargo_toml", &context)
|
||||
}
|
||||
|
||||
pub struct Metadata {
|
||||
name: String,
|
||||
authors: Vec<String>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub fn new(name: String, authors: Vec<String>) -> Self {
|
||||
Self { name, authors }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_files(matadata: &Metadata, network: &ProtoNetwork, compile_dir: &Path, input_type: &str, output_type: &str) -> anyhow::Result<()> {
|
||||
let src = compile_dir.join("src");
|
||||
let cargo_file = compile_dir.join("Cargo.toml");
|
||||
let cargo_toml = create_cargo_toml(matadata)?;
|
||||
std::fs::write(cargo_file, cargo_toml)?;
|
||||
|
||||
let toolchain_file = compile_dir.join("rust-toolchain");
|
||||
let toolchain = include_str!("templates/rust-toolchain");
|
||||
std::fs::write(toolchain_file, toolchain)?;
|
||||
|
||||
// create src dir
|
||||
match std::fs::create_dir(&src) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if e.kind() != std::io::ErrorKind::AlreadyExists {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let lib = src.join("lib.rs");
|
||||
let shader = serialize_gpu(network, input_type, output_type)?;
|
||||
println!("{}", shader);
|
||||
std::fs::write(lib, shader)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serialize_gpu(network: &ProtoNetwork, input_type: &str, output_type: &str) -> anyhow::Result<String> {
|
||||
assert_eq!(network.inputs.len(), 1);
|
||||
/*let input = &network.nodes[network.inputs[0] as usize].1;
|
||||
let output = &network.nodes[network.output as usize].1;
|
||||
let input_type = format!("{}::Input", input.identifier.fully_qualified_name());
|
||||
let output_type = format!("{}::Output", output.identifier.fully_qualified_name());
|
||||
*/
|
||||
|
||||
fn nid(id: &u64) -> String {
|
||||
format!("n{id}")
|
||||
}
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
#[derive(serde::Serialize)]
|
||||
struct Node {
|
||||
id: String,
|
||||
fqn: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
for (ref id, node) in network.nodes.iter() {
|
||||
let fqn = &node.identifier.name;
|
||||
let id = nid(id);
|
||||
|
||||
nodes.push(Node {
|
||||
id,
|
||||
fqn: fqn.to_string(),
|
||||
args: node.construction_args.new_function_args(),
|
||||
});
|
||||
}
|
||||
|
||||
let template = include_str!("templates/spirv-template.rs");
|
||||
let mut tera = tera::Tera::default();
|
||||
tera.add_raw_template("spirv", template)?;
|
||||
let mut context = Context::new();
|
||||
context.insert("input_type", &input_type);
|
||||
context.insert("output_type", &output_type);
|
||||
context.insert("nodes", &nodes);
|
||||
context.insert("last_node", &nid(&network.output));
|
||||
context.insert("compute_threads", &64);
|
||||
Ok(tera.render("spirv", &context)?)
|
||||
}
|
||||
|
||||
use spirv_builder::{MetadataPrintout, SpirvBuilder, SpirvMetadata};
|
||||
pub fn compile(dir: &Path) -> Result<spirv_builder::CompileResult, spirv_builder::SpirvBuilderError> {
|
||||
let result = SpirvBuilder::new(dir, "spirv-unknown-spv1.5")
|
||||
.print_metadata(MetadataPrintout::DependencyOnly)
|
||||
.multimodule(false)
|
||||
.preserve_bindings(true)
|
||||
.release(true)
|
||||
//.relax_struct_store(true)
|
||||
//.relax_block_layout(true)
|
||||
.spirv_metadata(SpirvMetadata::Full)
|
||||
.build()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
#[test]
|
||||
fn test_create_cargo_toml() {
|
||||
let cargo_toml = super::create_cargo_toml(&super::Metadata {
|
||||
name: "project".to_owned(),
|
||||
authors: vec!["Example <john.smith@example.com>".to_owned(), "smith.john@example.com".to_owned()],
|
||||
});
|
||||
let cargo_toml = cargo_toml.expect("failed to build carog toml template");
|
||||
let lines = cargo_toml.split('\n').collect::<Vec<_>>();
|
||||
let cargo_toml = lines[..lines.len() - 2].join("\n");
|
||||
let reference = r#"[package]
|
||||
name = "project-node"
|
||||
version = "0.1.0"
|
||||
authors = ["Example <john.smith@example.com>", "smith.john@example.com", ]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["dylib", "lib"]
|
||||
|
||||
[patch.crates-io]
|
||||
libm = { git = "https://github.com/rust-lang/libm", tag = "0.2.5" }
|
||||
|
||||
[dependencies]
|
||||
spirv-std = { git = "https://github.com/EmbarkStudios/rust-gpu" , features= ["glam"]}"#;
|
||||
|
||||
assert_eq!(cargo_toml, reference);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use vulkano::{
|
||||
command_buffer::allocator::StandardCommandBufferAllocator,
|
||||
descriptor_set::allocator::StandardDescriptorSetAllocator,
|
||||
device::{Device, DeviceCreateInfo, Queue, QueueCreateInfo},
|
||||
instance::{Instance, InstanceCreateInfo},
|
||||
memory::allocator::StandardMemoryAllocator,
|
||||
VulkanLibrary,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Context {
|
||||
pub instance: Arc<Instance>,
|
||||
pub device: Arc<Device>,
|
||||
pub queue: Arc<Queue>,
|
||||
pub allocator: StandardMemoryAllocator,
|
||||
pub command_buffer_allocator: StandardCommandBufferAllocator,
|
||||
pub descriptor_set_allocator: StandardDescriptorSetAllocator,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Self {
|
||||
let library = VulkanLibrary::new().unwrap();
|
||||
let instance = Instance::new(library, InstanceCreateInfo::default()).expect("failed to create instance");
|
||||
let physical = instance.enumerate_physical_devices().expect("could not enumerate devices").next().expect("no device available");
|
||||
for family in physical.queue_family_properties() {
|
||||
println!("Found a queue family with {:?} queue(s)", family.queue_count);
|
||||
}
|
||||
let queue_family_index = physical
|
||||
.queue_family_properties()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.position(|(_, q)| q.queue_flags.graphics)
|
||||
.expect("couldn't find a graphical queue family") as u32;
|
||||
|
||||
let (device, mut queues) = Device::new(
|
||||
physical,
|
||||
DeviceCreateInfo {
|
||||
// here we pass the desired queue family to use by index
|
||||
queue_create_infos: vec![QueueCreateInfo {
|
||||
queue_family_index,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to create device");
|
||||
let queue = queues.next().unwrap();
|
||||
let alloc = StandardMemoryAllocator::new_default(device.clone());
|
||||
let calloc = StandardCommandBufferAllocator::new(device.clone());
|
||||
let dalloc = StandardDescriptorSetAllocator::new(device.clone());
|
||||
|
||||
Self {
|
||||
instance,
|
||||
device,
|
||||
queue,
|
||||
allocator: alloc,
|
||||
command_buffer_allocator: calloc,
|
||||
descriptor_set_allocator: dalloc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use super::{compiler::Metadata, context::Context};
|
||||
use crate::{executor::Any, gpu::compiler};
|
||||
use bytemuck::Pod;
|
||||
use dyn_any::StaticTypeSized;
|
||||
use vulkano::{
|
||||
buffer::{self, BufferUsage, CpuAccessibleBuffer},
|
||||
command_buffer::{allocator::StandardCommandBufferAllocator, AutoCommandBufferBuilder, CommandBufferUsage},
|
||||
descriptor_set::{allocator::StandardDescriptorSetAllocator, PersistentDescriptorSet, WriteDescriptorSet},
|
||||
device::Device,
|
||||
memory::allocator::StandardMemoryAllocator,
|
||||
pipeline::{ComputePipeline, Pipeline, PipelineBindPoint},
|
||||
sync::GpuFuture,
|
||||
};
|
||||
|
||||
use crate::proto::*;
|
||||
use graphene_core::gpu::PushConstants;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GpuExecutor<I: StaticTypeSized, O> {
|
||||
context: Context,
|
||||
entry_point: String,
|
||||
shader: std::sync::Arc<vulkano::shader::ShaderModule>,
|
||||
_phantom: std::marker::PhantomData<(I, O)>,
|
||||
}
|
||||
|
||||
impl<I: StaticTypeSized, O> GpuExecutor<I, O> {
|
||||
pub fn new(context: Context, network: ProtoNetwork, metadata: Metadata, compile_dir: &Path) -> anyhow::Result<Self> {
|
||||
compiler::create_files(&metadata, &network, compile_dir, std::any::type_name::<I>(), std::any::type_name::<O>())?;
|
||||
let result = compiler::compile(compile_dir)?;
|
||||
|
||||
let bytes = std::fs::read(result.module.unwrap_single())?;
|
||||
let shader = unsafe { vulkano::shader::ShaderModule::from_bytes(context.device.clone(), &bytes)? };
|
||||
let entry_point = result.entry_points.first().expect("No entry points").clone();
|
||||
|
||||
Ok(Self {
|
||||
context,
|
||||
entry_point,
|
||||
shader,
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: StaticTypeSized + Sync + Pod + Send, O: StaticTypeSized + Send + Sync + Pod> crate::executor::Executor for GpuExecutor<I, O> {
|
||||
fn execute(&self, input: Any<'static>) -> Result<Any<'static>, Box<dyn std::error::Error>> {
|
||||
let input = dyn_any::downcast::<Vec<I>>(input).expect("Wrong input type");
|
||||
let context = &self.context;
|
||||
let result: Vec<O> = execute_shader(
|
||||
context.device.clone(),
|
||||
context.queue.clone(),
|
||||
self.shader.entry_point(&self.entry_point).expect("Entry point not found in shader"),
|
||||
&context.allocator,
|
||||
&context.command_buffer_allocator,
|
||||
*input,
|
||||
);
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_shader<I: Pod + Send + Sync, O: Pod + Send + Sync>(
|
||||
device: std::sync::Arc<Device>,
|
||||
queue: std::sync::Arc<vulkano::device::Queue>,
|
||||
entry_point: vulkano::shader::EntryPoint,
|
||||
alloc: &StandardMemoryAllocator,
|
||||
calloc: &StandardCommandBufferAllocator,
|
||||
data: Vec<I>,
|
||||
) -> Vec<O> {
|
||||
let constants = PushConstants { n: data.len() as u32, node: 0 };
|
||||
|
||||
let dest_data: Vec<_> = (0..constants.n).map(|_| O::zeroed()).collect();
|
||||
let source_buffer = create_buffer(data, alloc).expect("failed to create buffer");
|
||||
let dest_buffer = create_buffer(dest_data, alloc).expect("failed to create buffer");
|
||||
|
||||
let compute_pipeline = ComputePipeline::new(device.clone(), entry_point, &(), None, |_| {}).expect("failed to create compute pipeline");
|
||||
let layout = compute_pipeline.layout().set_layouts().get(0).unwrap();
|
||||
let dalloc = StandardDescriptorSetAllocator::new(device.clone());
|
||||
let set = PersistentDescriptorSet::new(
|
||||
&dalloc,
|
||||
layout.clone(),
|
||||
[
|
||||
WriteDescriptorSet::buffer(0, source_buffer), // 0 is the binding
|
||||
WriteDescriptorSet::buffer(1, dest_buffer.clone()),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let mut builder = AutoCommandBufferBuilder::primary(calloc, queue.queue_family_index(), CommandBufferUsage::OneTimeSubmit).unwrap();
|
||||
|
||||
builder
|
||||
.bind_pipeline_compute(compute_pipeline.clone())
|
||||
.bind_descriptor_sets(PipelineBindPoint::Compute, compute_pipeline.layout().clone(), 0, set)
|
||||
.push_constants(compute_pipeline.layout().clone(), 0, constants)
|
||||
.dispatch([((constants.n as isize - 1) / 1024 + 1) as u32 * 1024, 1, 1])
|
||||
.unwrap();
|
||||
let command_buffer = builder.build().unwrap();
|
||||
|
||||
let future = vulkano::sync::now(device).then_execute(queue, command_buffer).unwrap().then_signal_fence_and_flush().unwrap();
|
||||
#[cfg(feature = "profiling")]
|
||||
nvtx::range_push!("compute");
|
||||
future.wait(None).unwrap();
|
||||
#[cfg(feature = "profiling")]
|
||||
nvtx::range_pop!();
|
||||
let content = dest_buffer.read().unwrap();
|
||||
content.to_vec()
|
||||
}
|
||||
|
||||
fn create_buffer<T: Pod + Send + Sync>(data: Vec<T>, alloc: &StandardMemoryAllocator) -> Result<std::sync::Arc<CpuAccessibleBuffer<[T]>>, vulkano::memory::allocator::AllocationCreationError> {
|
||||
let buffer_usage = BufferUsage {
|
||||
storage_buffer: true,
|
||||
transfer_src: true,
|
||||
transfer_dst: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
buffer::CpuAccessibleBuffer::from_iter(alloc, buffer_usage, false, data.into_iter())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::concrete;
|
||||
use crate::generic;
|
||||
use crate::gpu::compiler;
|
||||
|
||||
fn inc_network() -> ProtoNetwork {
|
||||
let mut construction_network = ProtoNetwork {
|
||||
inputs: vec![10],
|
||||
output: 1,
|
||||
nodes: [
|
||||
(
|
||||
1,
|
||||
ProtoNode {
|
||||
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[generic!("u32")]),
|
||||
input: ProtoNodeInput::Node(11),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
},
|
||||
),
|
||||
(
|
||||
10,
|
||||
ProtoNode {
|
||||
identifier: NodeIdentifier::new("graphene_core::structural::ConsNode", &[generic!("&ValueNode<u32>"), generic!("()")]),
|
||||
input: ProtoNodeInput::Network,
|
||||
construction_args: ConstructionArgs::Nodes(vec![14]),
|
||||
},
|
||||
),
|
||||
(
|
||||
11,
|
||||
ProtoNode {
|
||||
identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[generic!("u32"), generic!("u32")]),
|
||||
input: ProtoNodeInput::Node(10),
|
||||
construction_args: ConstructionArgs::Nodes(vec![]),
|
||||
},
|
||||
),
|
||||
(
|
||||
14,
|
||||
ProtoNode {
|
||||
identifier: NodeIdentifier::new("graphene_core::value::ValueNode", &[concrete!("u32")]),
|
||||
input: ProtoNodeInput::None,
|
||||
construction_args: ConstructionArgs::Value(Box::new(3_u32)),
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
construction_network.resolve_inputs();
|
||||
construction_network.reorder_ids();
|
||||
construction_network
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_on_gpu() {
|
||||
use crate::executor::Executor;
|
||||
let m = compiler::Metadata::new("project".to_owned(), vec!["test@example.com".to_owned()]);
|
||||
let network = inc_network();
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create tempdir");
|
||||
|
||||
let executor: GpuExecutor<u32, u32> = GpuExecutor::new(Context::new(), network, m, temp_dir.path()).unwrap();
|
||||
|
||||
let data: Vec<_> = (0..1024).map(|x| x as u32).collect();
|
||||
let result = executor.execute(Box::new(data)).unwrap();
|
||||
let result = dyn_any::downcast::<Vec<u32>>(result).unwrap();
|
||||
for (i, r) in result.iter().enumerate() {
|
||||
assert_eq!(*r, i as u32 + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "{{name}}-node"
|
||||
version = "0.1.0"
|
||||
authors = [{%for author in authors%}"{{author}}", {%endfor%}]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["dylib", "lib"]
|
||||
|
||||
[patch.crates-io]
|
||||
libm = { git = "https://github.com/rust-lang/libm", tag = "0.2.5" }
|
||||
|
||||
[dependencies]
|
||||
spirv-std = { git = "https://github.com/EmbarkStudios/rust-gpu" , features= ["glam"]}
|
||||
graphene-core = {path = "{{gcore_path}}", default-features = false, features = ["gpu"]}
|
||||
@@ -1,38 +0,0 @@
|
||||
#![no_std]
|
||||
#![feature(unchecked_math)]
|
||||
#![deny(warnings)]
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
extern crate spirv_std;
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
pub mod gpu {
|
||||
use super::*;
|
||||
use spirv_std::spirv;
|
||||
use spirv_std::glam::UVec3;
|
||||
|
||||
#[allow(unused)]
|
||||
#[spirv(compute(threads({{compute_threads}})))]
|
||||
pub fn eval (
|
||||
#[spirv(global_invocation_id)] global_id: UVec3,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] a: &[{{input_type}}],
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] y: &mut [{{output_type}}],
|
||||
#[spirv(push_constant)] push_consts: &graphene_core::gpu::PushConstants,
|
||||
) {
|
||||
let gid = global_id.x as usize;
|
||||
// Only process up to n, which is the length of the buffers.
|
||||
if global_id.x < push_consts.n {
|
||||
y[gid] = node_graph(a[gid]);
|
||||
}
|
||||
}
|
||||
|
||||
fn node_graph(input: {{input_type}}) -> {{output_type}} {
|
||||
use graphene_core::Node;
|
||||
|
||||
{% for node in nodes %}
|
||||
let {{node.id}} = {{node.fqn}}::new({% for arg in node.args %}{{arg}}, {% endfor %});
|
||||
{% endfor %}
|
||||
{{last_node}}.eval(input)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,3 @@ pub mod proto;
|
||||
|
||||
pub mod executor;
|
||||
pub mod imaginate_input;
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
pub mod gpu;
|
||||
|
||||
Reference in New Issue
Block a user