use std::sync::OnceLock;
use xla::{ArrayElement, Literal, PjRtBuffer, PjRtClient, PjRtLoadedExecutable, XlaBuilder};
use crate::simulate_array::ArrayCompiled;
use crate::simulate_array::tape::xla_emit::{self, EmittedRhs, ForcingFeed, XlaEmitError};
#[derive(Debug)]
pub enum CompileRhsError {
Refused(XlaEmitError),
Runtime(String),
}
impl std::fmt::Display for CompileRhsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompileRhsError::Refused(e) => write!(f, "refused: {e}"),
CompileRhsError::Runtime(m) => write!(f, "xla runtime: {m}"),
}
}
}
impl std::error::Error for CompileRhsError {}
impl From<XlaEmitError> for CompileRhsError {
fn from(e: XlaEmitError) -> Self {
CompileRhsError::Refused(e)
}
}
pub fn client() -> Result<&'static PjRtClient, String> {
static CLIENT: OnceLock<Result<PjRtClient, String>> = OnceLock::new();
CLIENT
.get_or_init(|| match std::env::var("EARTHSCI_XLA_PLATFORM").as_deref() {
Ok("gpu") | Ok("cuda") => {
let frac = std::env::var("EARTHSCI_XLA_GPU_MEMORY_FRACTION")
.ok()
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.75);
let prealloc = std::env::var("EARTHSCI_XLA_GPU_PREALLOCATE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
PjRtClient::gpu(frac, prealloc)
.map_err(|e| format!("PjRtClient::gpu failed ({e}).\n{}", gpu_setup_hint()))
}
Ok("cpu") | Err(_) => {
PjRtClient::cpu().map_err(|e| format!("PjRtClient::cpu failed: {e}"))
}
Ok(other) => Err(format!(
"EARTHSCI_XLA_PLATFORM={other:?} is not a platform this build knows \
(want cpu|gpu)"
)),
})
.as_ref()
.map_err(|e| e.clone())
}
fn gpu_setup_hint() -> String {
let mut out = String::new();
match std::env::var_os("XLA_EXTENSION_DIR") {
Some(dir) => {
let dir = std::path::PathBuf::from(dir);
let cuda_build = dir.join("include/_virtual_includes/cuda_headers").is_dir();
if cuda_build {
out.push_str(&format!(
"The extension at {} is a CUDA build, so this is most likely no \
visible device: check that the job asked for one (nvidia-smi, \
CUDA_VISIBLE_DEVICES).\n",
dir.display()
));
} else {
out.push_str(&format!(
"The extension at {} looks like the CPU build (it has no CUDA \
headers), which has no GPU backend compiled in. Fetch the CUDA \
one and rebuild against it:\n \
scripts/fetch-xla-extension.sh --variant cuda12 --dest <dir outside the repo>\n \
export XLA_EXTENSION_DIR=<that dir>/xla_extension-<version>-cuda12/xla_extension\n \
cargo build --features xla ... # the rpath is baked at build time\n",
dir.display()
));
}
}
None => out.push_str(
"XLA_EXTENSION_DIR is unset here, so this process is running against \
whatever extension was baked into its rpath at build time; if that was \
the CPU build there is no GPU backend to start.\n",
),
}
out.push_str(
"The CUDA extension also HARD-LINKS cuDNN, NCCL, nvshmem, cuBLAS, cuFFT, \
cuSPARSE, nvJitLink, the CUDA runtime and NVRTC, and needs ptxas through \
XLA_FLAGS=--xla_gpu_cuda_data_dir=... Build that environment with\n \
scripts/setup-xla-gpu-libs.sh --prefix <dir outside the repo>\n\
and export the lines it prints. (A library missing outright does not reach \
this message at all: it is a dynamic-loader failure before main, reading \
\"error while loading shared libraries\".)",
);
out
}
pub struct CompiledRhs {
exe: PjRtLoadedExecutable,
n_states: usize,
params_len: usize,
n_instrs: usize,
opcode_counts: Vec<(&'static str, usize)>,
platform: String,
forcing: Option<ForcingFeed>,
}
impl CompiledRhs {
pub fn compile(model: &ArrayCompiled) -> Result<Self, CompileRhsError> {
let emitted = xla_emit::emit_rhs(model)?;
Self::from_emitted(emitted)
}
pub fn from_emitted(mut emitted: EmittedRhs) -> Result<Self, CompileRhsError> {
let forcing = emitted.take_forcing();
let client = client().map_err(CompileRhsError::Runtime)?;
let exe = client
.compile(emitted.computation())
.map_err(|e| CompileRhsError::Runtime(format!("XLA compilation failed: {e}")))?;
Ok(CompiledRhs {
exe,
n_states: emitted.n_states(),
params_len: emitted.params_len(),
n_instrs: emitted.n_instrs(),
opcode_counts: emitted.opcode_counts().to_vec(),
platform: client.platform_name(),
forcing,
})
}
pub fn n_states(&self) -> usize {
self.n_states
}
pub fn params_len(&self) -> usize {
self.params_len
}
pub fn n_instrs(&self) -> usize {
self.n_instrs
}
pub fn opcode_count(&self, op: &str) -> usize {
self.opcode_counts
.iter()
.find(|(o, _)| *o == op)
.map_or(0, |(_, n)| *n)
}
pub fn platform(&self) -> &str {
&self.platform
}
pub fn eval(&self, state: &[f64], params: &[f64], t: f64) -> Result<Vec<f64>, CompileRhsError> {
let mut du = vec![0.0f64; self.n_states];
self.eval_into(state, params, t, &mut du)?;
Ok(du)
}
pub fn eval_into(
&self,
state: &[f64],
params: &[f64],
t: f64,
out: &mut [f64],
) -> Result<(), CompileRhsError> {
if out.len() != self.n_states {
return Err(CompileRhsError::Runtime(format!(
"output buffer has {} elements, the compiled program returns {}",
out.len(),
self.n_states
)));
}
let pv = self.check_and_pad(state, params)?;
let mut args = vec![Literal::vec1(state), Literal::vec1(&pv), Literal::scalar(t)];
if let Some(feed) = &self.forcing {
args.push(Literal::vec1(&feed.pack()));
}
let result = self
.exe
.execute::<Literal>(&args)
.map_err(|e| CompileRhsError::Runtime(format!("execute failed: {e}")))?;
let buf = result
.first()
.and_then(|replica| replica.first())
.ok_or_else(|| CompileRhsError::Runtime("execute returned no buffer".into()))?;
let returned = buf
.on_device_shape()
.and_then(|s| xla::ArrayShape::try_from(&s))
.map_err(|e| CompileRhsError::Runtime(format!("copy back failed: {e}")))?
.element_count();
if returned != self.n_states {
return Err(CompileRhsError::Runtime(format!(
"compiled program returned {returned} elements, expected {}",
self.n_states
)));
}
buf.copy_raw_to_host_sync(out, 0)
.map_err(|e| CompileRhsError::Runtime(format!("copy back failed: {e}")))
}
}
impl CompiledRhs {
pub fn execute_on_buffers(
&self,
u: &PjRtBuffer,
p: &PjRtBuffer,
t: &PjRtBuffer,
) -> Result<Vec<f64>, CompileRhsError> {
if self.forcing.is_some() {
return Err(CompileRhsError::Runtime(
"this program reads the forcing buffer and takes a fourth argument, which \
this three-buffer seam does not supply; use `eval` or `on_device`"
.into(),
));
}
let out = self
.exe
.execute_b(&[u, p, t])
.map_err(|e| CompileRhsError::Runtime(format!("execute failed: {e}")))?;
let buf = take_single_output(out, "rhs")?;
let mut host = vec![0.0f64; self.n_states];
buf.copy_raw_to_host_sync(&mut host, 0)
.map_err(|e| CompileRhsError::Runtime(format!("copy back failed: {e}")))?;
Ok(host)
}
}
pub struct DeviceRhs<'a> {
rhs: &'a CompiledRhs,
u: PjRtBuffer,
p: PjRtBuffer,
f: Option<PjRtBuffer>,
du: Option<PjRtBuffer>,
stepper: Option<PjRtLoadedExecutable>,
}
impl CompiledRhs {
pub fn on_device<'a>(
&'a self,
state: &[f64],
params: &[f64],
) -> Result<DeviceRhs<'a>, CompileRhsError> {
let pv = self.check_and_pad(state, params)?;
let client = client().map_err(CompileRhsError::Runtime)?;
let u = client
.buffer_from_host_buffer(state, &[self.n_states], None)
.map_err(|e| CompileRhsError::Runtime(format!("upload of u failed: {e}")))?;
let p = client
.buffer_from_host_buffer(&pv, &[self.params_len], None)
.map_err(|e| CompileRhsError::Runtime(format!("upload of p failed: {e}")))?;
let f = match &self.forcing {
None => None,
Some(feed) => Some(
client
.buffer_from_host_buffer(&feed.pack(), &[feed.len()], None)
.map_err(|e| CompileRhsError::Runtime(format!("upload of f failed: {e}")))?,
),
};
Ok(DeviceRhs {
rhs: self,
u,
p,
f,
du: None,
stepper: None,
})
}
fn check_and_pad(&self, state: &[f64], params: &[f64]) -> Result<Vec<f64>, CompileRhsError> {
if state.len() != self.n_states {
return Err(CompileRhsError::Runtime(format!(
"state has {} elements, the compiled program takes {}",
state.len(),
self.n_states
)));
}
if params.len() > self.params_len {
return Err(CompileRhsError::Runtime(format!(
"parameter vector has {} elements, the compiled program takes {}",
params.len(),
self.params_len
)));
}
let mut pv = vec![0.0f64; self.params_len];
pv[..params.len()].copy_from_slice(params);
Ok(pv)
}
}
impl DeviceRhs<'_> {
pub fn n_states(&self) -> usize {
self.rhs.n_states
}
pub fn set_state(&mut self, state: &[f64]) -> Result<(), CompileRhsError> {
if state.len() != self.rhs.n_states {
return Err(CompileRhsError::Runtime(format!(
"state has {} elements, the compiled program takes {}",
state.len(),
self.rhs.n_states
)));
}
let client = client().map_err(CompileRhsError::Runtime)?;
self.u = client
.buffer_from_host_buffer(state, &[self.rhs.n_states], None)
.map_err(|e| CompileRhsError::Runtime(format!("upload of u failed: {e}")))?;
Ok(())
}
pub fn eval_at(&mut self, t: f64) -> Result<(), CompileRhsError> {
let client = client().map_err(CompileRhsError::Runtime)?;
let tb = client
.buffer_from_host_buffer(&[t], &[], None)
.map_err(|e| CompileRhsError::Runtime(format!("upload of t failed: {e}")))?;
let out = match &self.f {
None => self.rhs.exe.execute_b(&[&self.u, &self.p, &tb]),
Some(f) => self.rhs.exe.execute_b(&[&self.u, &self.p, &tb, f]),
}
.map_err(|e| CompileRhsError::Runtime(format!("execute failed: {e}")))?;
self.du = Some(take_single_output(out, "rhs")?);
Ok(())
}
pub fn du_device(&self) -> Option<&PjRtBuffer> {
self.du.as_ref()
}
pub fn du_to_host(&self) -> Result<Vec<f64>, CompileRhsError> {
let du = self.du.as_ref().ok_or_else(|| {
CompileRhsError::Runtime("no derivative on the device yet; call eval_at first".into())
})?;
let mut host = vec![0.0f64; self.rhs.n_states];
du.copy_raw_to_host_sync(&mut host, 0)
.map_err(|e| CompileRhsError::Runtime(format!("copy back failed: {e}")))?;
Ok(host)
}
pub fn state_to_host(&self) -> Result<Vec<f64>, CompileRhsError> {
let mut host = vec![0.0f64; self.rhs.n_states];
self.u
.copy_raw_to_host_sync(&mut host, 0)
.map_err(|e| CompileRhsError::Runtime(format!("copy back failed: {e}")))?;
Ok(host)
}
pub fn euler_step(&mut self, t: f64, dt: f64) -> Result<(), CompileRhsError> {
self.eval_at(t)?;
let client = client().map_err(CompileRhsError::Runtime)?;
if self.stepper.is_none() {
self.stepper = Some(compile_axpy(self.rhs.n_states)?);
}
let dtb = client
.buffer_from_host_buffer(&[dt], &[], None)
.map_err(|e| CompileRhsError::Runtime(format!("upload of dt failed: {e}")))?;
let du = self.du.as_ref().expect("eval_at just set the derivative");
let out = self
.stepper
.as_ref()
.expect("just compiled")
.execute_b(&[&self.u, du, &dtb])
.map_err(|e| CompileRhsError::Runtime(format!("step execute failed: {e}")))?;
self.u = take_single_output(out, "step")?;
Ok(())
}
}
fn take_single_output(
mut out: Vec<Vec<PjRtBuffer>>,
what: &str,
) -> Result<PjRtBuffer, CompileRhsError> {
if out.len() != 1 {
return Err(CompileRhsError::Runtime(format!(
"{what}: execute returned {} replicas, expected 1",
out.len()
)));
}
let mut replica = out.remove(0);
if replica.len() != 1 {
return Err(CompileRhsError::Runtime(format!(
"{what}: execute returned {} output buffers, expected 1",
replica.len()
)));
}
Ok(replica.remove(0))
}
fn compile_axpy(n: usize) -> Result<PjRtLoadedExecutable, CompileRhsError> {
let b = XlaBuilder::new("euler_step");
let build = || -> Result<xla::XlaComputation, xla::Error> {
let u = b.parameter(0, f64::TY, &[n as i64], "u")?;
let du = b.parameter(1, f64::TY, &[n as i64], "du")?;
let dt = b.parameter(2, f64::TY, &[], "dt")?;
let scaled = dt.broadcast(&[n as i64])?.mul_(&du)?;
b.build(&u.add_(&scaled)?)
};
let comp = build()
.map_err(|e| CompileRhsError::Runtime(format!("building the step program failed: {e}")))?;
client()
.map_err(CompileRhsError::Runtime)?
.compile(&comp)
.map_err(|e| CompileRhsError::Runtime(format!("compiling the step program failed: {e}")))
}