Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@ jobs:
include:
- name: ubuntu-latest / stable
os: ubuntu-latest
native_aot_smoke_target: ""
- name: macos-14 / stable
os: macos-14
native_aot_smoke_target: x86_64-apple-darwin
- name: windows-latest / stable
os: windows-latest
native_aot_smoke_target: ""
steps:
- name: Checkout code
uses: actions/checkout@v7
Expand All @@ -44,10 +41,6 @@ jobs:
with:
components: rustfmt, clippy

- name: Install Native AoT smoke target
if: ${{ matrix.native_aot_smoke_target != '' }}
run: rustup target add ${{ matrix.native_aot_smoke_target }}

- name: Setup cache
uses: Swatinem/rust-cache@v2
with:
Expand Down Expand Up @@ -75,8 +68,6 @@ jobs:
run: cargo test

- name: Run tests (all features)
env:
PHARMSOL_NATIVE_AOT_SMOKE_TARGET: ${{ matrix.native_aot_smoke_target }}
run: cargo test --all-features

- name: Run doc tests
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ cargo.lock
Cargo.lock
/.vscode
/.idea
*.pkm
/paper_files
paper.html
/joss/paper_files
/tests/browser-e2e/node_modules/
docs/
7 changes: 1 addition & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,10 @@ dsl-jit = [
"dep:cranelift-module",
"dep:cranelift-native",
]
dsl-aot = ["dsl-core"]
dsl-aot-load = ["dsl-core", "dep:libloading"]

[dependencies]
pharmsol-dsl = { workspace = true }
pharmsol-macros = { workspace = true }
libloading = { version = "0.9.0", optional = true, features = [] }
cranelift = { version = "0.134.3", optional = true }
cranelift-jit = { version = "0.134.3", optional = true }
cranelift-module = { version = "0.134.3", optional = true }
Expand All @@ -70,8 +67,6 @@ quick_cache = "0.7.0"
criterion = { version = "0.8.2", features = ["html_reports"] }
approx = "0.5.1"
tempfile = "3.27.0"
tiny_http = "0.12.0"
webbrowser = "1.2.1"

[lib]
bench = false
Expand All @@ -83,4 +78,4 @@ harness = false
[[bench]]
name = "dsl_matrix"
harness = false
required-features = ["dsl-jit", "dsl-aot", "dsl-aot-load"]
required-features = ["dsl-jit"]
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,13 @@ see [docs/analytical-authoring-migration.md](docs/analytical-authoring-migration
## DSL and Runtime Targets

If the model needs to be loaded or compiled at runtime, pharmsol also provides a DSL with
the same broad modeling coverage: ODE, analytical, and SDE authoring. The DSL can target
an in-process JIT runtime or native ahead-of-time artifacts depending on how you want to
ship and execute the model.

- `dsl-jit`: compile DSL source into a runtime model inside the current process.
- `dsl-aot` and `dsl-aot-load`: emit a native artifact and load it later.
the same broad modeling coverage: ODE, analytical, and SDE authoring. Enable the `dsl-jit`
feature to compile DSL source into a runtime model inside the current process.

See [examples/dsl_runtime_jit.rs](examples/dsl_runtime_jit.rs) for the in-repo JIT flow and
[examples/dsl_jit_analytical_covariates.rs](examples/dsl_jit_analytical_covariates.rs) for a
small analytical covariate example written both as DSL JIT source and as an
`analytical!` model.
The companion `pharmsol-examples` crate includes an end-to-end native AOT runtime example.

## Performance

Expand Down
120 changes: 31 additions & 89 deletions benches/dsl_matrix.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! DSL bench matrix (feature-gated): JIT, native AoT across all workloads + solvers.
//! DSL bench matrix (feature-gated): JIT across all workloads + solvers.
//! Mirrors `native_matrix.rs` but compiles models from DSL source.
//!
//! IDs:
Expand All @@ -8,15 +8,13 @@
//! - `dsl/likelihood-matrix` → `{workload}/{kind}/{backend}`

use std::hint::black_box;
use std::path::PathBuf;
use std::time::Duration;

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode};
use tempfile::TempDir;

use pharmsol::dsl::{
compile_module_source_to_runtime, CompiledRuntimeModel, NativeAnalyticalModel,
NativeAotCompileOptions, NativeOdeModel, NativeSdeModel, RuntimeCompilationTarget,
compile_module_source_to_runtime, CompiledRuntimeModel, NativeAnalyticalModel, NativeOdeModel,
NativeSdeModel, RuntimeCompilationTarget,
};
use pharmsol::prelude::*;
use pharmsol::{Cache, Parameters};
Expand All @@ -31,21 +29,17 @@ const MATRIX_N_SUBJECTS: usize = 32;
const MATRIX_N_SUPPORT: usize = 64;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] // Aot temporarily disabled in `Backend::all`
enum Backend {
Jit,
Aot,
}

impl Backend {
fn label(self) -> &'static str {
match self {
Self::Jit => "dsl-jit",
Self::Aot => "dsl-aot",
}
}

// AoT backend temporarily disabled — too slow for the current matrix.
fn all() -> [Backend; 1] {
[Backend::Jit]
}
Expand All @@ -70,52 +64,19 @@ impl CacheState {
}
}

/// One `TempDir` shared across the bench binary; each compile gets a fresh subdir.
struct AotWorkspace {
root: TempDir,
counter: std::cell::Cell<usize>,
}

impl AotWorkspace {
fn new() -> Self {
Self {
root: tempfile::Builder::new()
.prefix("pharmsol-bench-dsl-aot-")
.tempdir()
.expect("create AoT workspace tempdir"),
counter: std::cell::Cell::new(0),
}
}

fn fresh(&self, stem: &str) -> PathBuf {
let n = self.counter.get();
self.counter.set(n + 1);
self.root.path().join(format!("{stem}-{n:04}"))
}
}

/// Compile `(workload, kind)` with `backend` and return the full `CompiledRuntimeModel`.
fn compile_runtime(
workload: Workload,
kind: SolverKind,
backend: Backend,
aot: &AotWorkspace,
) -> CompiledRuntimeModel {
fn compile_runtime(workload: Workload, kind: SolverKind, backend: Backend) -> CompiledRuntimeModel {
let source = dsl_source(workload, kind);
let name = dsl_model_name(workload, kind);
let target = match backend {
Backend::Jit => RuntimeCompilationTarget::Jit,
Backend::Aot => {
let dir = aot.fresh(&format!("{}-{}", workload.label(), kind.label()));
RuntimeCompilationTarget::NativeAot(NativeAotCompileOptions::new(dir))
}
};
compile_module_source_to_runtime(source, Some(name), target, |_, _| {})
.unwrap_or_else(|e| panic!("compile {} via {} failed: {e:?}", name, backend.label()))
}

fn compile_ode(workload: Workload, backend: Backend, aot: &AotWorkspace) -> NativeOdeModel {
match compile_runtime(workload, SolverKind::Ode, backend, aot) {
fn compile_ode(workload: Workload, backend: Backend) -> NativeOdeModel {
match compile_runtime(workload, SolverKind::Ode, backend) {
CompiledRuntimeModel::Ode(model) => model,
other => panic!(
"expected Ode model for {}, got {:?}",
Expand All @@ -125,12 +86,8 @@ fn compile_ode(workload: Workload, backend: Backend, aot: &AotWorkspace) -> Nati
}
}

fn compile_analytical(
workload: Workload,
backend: Backend,
aot: &AotWorkspace,
) -> NativeAnalyticalModel {
match compile_runtime(workload, SolverKind::Analytical, backend, aot) {
fn compile_analytical(workload: Workload, backend: Backend) -> NativeAnalyticalModel {
match compile_runtime(workload, SolverKind::Analytical, backend) {
CompiledRuntimeModel::Analytical(model) => model,
other => panic!(
"expected Analytical model for {}, got {:?}",
Expand All @@ -140,8 +97,8 @@ fn compile_analytical(
}
}

fn compile_sde(workload: Workload, backend: Backend, aot: &AotWorkspace) -> NativeSdeModel {
match compile_runtime(workload, SolverKind::Sde, backend, aot) {
fn compile_sde(workload: Workload, backend: Backend) -> NativeSdeModel {
match compile_runtime(workload, SolverKind::Sde, backend) {
CompiledRuntimeModel::Sde(model) => model,
other => panic!(
"expected Sde model for {}, got {:?}",
Expand Down Expand Up @@ -175,16 +132,14 @@ fn compile_group(c: &mut Criterion) {
group.sampling_mode(SamplingMode::Flat);
group.sample_size(10);
group.measurement_time(Duration::from_secs(5));
// Each compile leaks an executable mmap (JIT) or runs rustc (AoT). Without
// a cap, a fast JIT compile (~60 µs) lets Criterion request hundreds of
// thousands of iterations per cell and exhausts the runner's executable
// memory pool / `vm.max_map_count`. We hard-cap real iterations per
// Criterion batch to `MAX_ITERS_PER_BATCH` and scale the reported elapsed
// time linearly so per-iteration timings stay accurate.
// Each compile leaks an executable mmap. Without a cap, a fast JIT compile
// (~60 µs) lets Criterion request hundreds of thousands of iterations per
// cell and exhausts the runner's executable memory pool /
// `vm.max_map_count`. We hard-cap real iterations per Criterion batch to
// `MAX_ITERS_PER_BATCH` and scale the reported elapsed time linearly so
// per-iteration timings stay accurate.
const MAX_ITERS_PER_BATCH: u64 = 25;

let aot = AotWorkspace::new();

for workload in Workload::all() {
for kind in SolverKind::all() {
for backend in Backend::all() {
Expand All @@ -203,7 +158,6 @@ fn compile_group(c: &mut Criterion) {
black_box(workload),
black_box(kind),
black_box(backend),
&aot,
));
}
let elapsed = start.elapsed();
Expand All @@ -223,8 +177,6 @@ fn predictions_group(c: &mut Criterion) {
let mut group = c.benchmark_group("dsl/predictions");
group.sampling_mode(SamplingMode::Flat);

let aot = AotWorkspace::new();

for workload in Workload::all() {
let subject = subject_for_predictions(workload);
for kind in SolverKind::all() {
Expand All @@ -240,10 +192,8 @@ fn predictions_group(c: &mut Criterion) {
match kind {
SolverKind::Ode => {
let model = match cache {
CacheState::Hot => compile_ode(workload, backend, &aot),
CacheState::Cold => {
compile_ode(workload, backend, &aot).disable_cache()
}
CacheState::Hot => compile_ode(workload, backend),
CacheState::Cold => compile_ode(workload, backend).disable_cache(),
};
let theta = ode_parameters(&model, workload);
group.bench_function(bench_id, |b| {
Expand All @@ -261,9 +211,9 @@ fn predictions_group(c: &mut Criterion) {
}
SolverKind::Analytical => {
let model = match cache {
CacheState::Hot => compile_analytical(workload, backend, &aot),
CacheState::Hot => compile_analytical(workload, backend),
CacheState::Cold => {
compile_analytical(workload, backend, &aot).disable_cache()
compile_analytical(workload, backend).disable_cache()
}
};
let theta = analytical_parameters(&model, workload);
Expand All @@ -282,10 +232,8 @@ fn predictions_group(c: &mut Criterion) {
}
SolverKind::Sde => {
let model = match cache {
CacheState::Hot => compile_sde(workload, backend, &aot),
CacheState::Cold => {
compile_sde(workload, backend, &aot).disable_cache()
}
CacheState::Hot => compile_sde(workload, backend),
CacheState::Cold => compile_sde(workload, backend).disable_cache(),
};
let theta = sde_parameters(&model, workload);
group.bench_function(bench_id, |b| {
Expand Down Expand Up @@ -316,7 +264,6 @@ fn log_likelihood_group(c: &mut Criterion) {
let mut group = c.benchmark_group("dsl/log-likelihood");
group.sampling_mode(SamplingMode::Flat);

let aot = AotWorkspace::new();
let error_models = assay_error_models();

for workload in Workload::all() {
Expand All @@ -334,10 +281,8 @@ fn log_likelihood_group(c: &mut Criterion) {
match kind {
SolverKind::Ode => {
let model = match cache {
CacheState::Hot => compile_ode(workload, backend, &aot),
CacheState::Cold => {
compile_ode(workload, backend, &aot).disable_cache()
}
CacheState::Hot => compile_ode(workload, backend),
CacheState::Cold => compile_ode(workload, backend).disable_cache(),
};
let theta = ode_parameters(&model, workload);
group.bench_function(bench_id, |b| {
Expand All @@ -356,9 +301,9 @@ fn log_likelihood_group(c: &mut Criterion) {
}
SolverKind::Analytical => {
let model = match cache {
CacheState::Hot => compile_analytical(workload, backend, &aot),
CacheState::Hot => compile_analytical(workload, backend),
CacheState::Cold => {
compile_analytical(workload, backend, &aot).disable_cache()
compile_analytical(workload, backend).disable_cache()
}
};
let theta = analytical_parameters(&model, workload);
Expand All @@ -378,10 +323,8 @@ fn log_likelihood_group(c: &mut Criterion) {
}
SolverKind::Sde => {
let model = match cache {
CacheState::Hot => compile_sde(workload, backend, &aot),
CacheState::Cold => {
compile_sde(workload, backend, &aot).disable_cache()
}
CacheState::Hot => compile_sde(workload, backend),
CacheState::Cold => compile_sde(workload, backend).disable_cache(),
};
let theta = sde_parameters(&model, workload);
group.bench_function(bench_id, |b| {
Expand Down Expand Up @@ -417,7 +360,6 @@ fn likelihood_matrix_group(c: &mut Criterion) {
group.sample_size(10);
group.measurement_time(Duration::from_secs(20));

let aot = AotWorkspace::new();
let error_models = assay_error_models();

for workload in Workload::all() {
Expand All @@ -433,7 +375,7 @@ fn likelihood_matrix_group(c: &mut Criterion) {
));
match kind {
SolverKind::Ode => {
let model = compile_ode(workload, backend, &aot);
let model = compile_ode(workload, backend);
group.bench_function(bench_id, |b| {
b.iter(|| {
black_box(
Expand All @@ -450,7 +392,7 @@ fn likelihood_matrix_group(c: &mut Criterion) {
});
}
SolverKind::Analytical => {
let model = compile_analytical(workload, backend, &aot);
let model = compile_analytical(workload, backend);
group.bench_function(bench_id, |b| {
b.iter(|| {
black_box(
Expand All @@ -467,7 +409,7 @@ fn likelihood_matrix_group(c: &mut Criterion) {
});
}
SolverKind::Sde => {
let model = compile_sde(workload, backend, &aot);
let model = compile_sde(workload, backend);
group.bench_function(bench_id, |b| {
b.iter(|| {
black_box(
Expand Down
4 changes: 2 additions & 2 deletions pharmsol-dsl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Use this crate when you need to work with model source as data:
- analyze names and types into a checked model
- compile validated models into the ready-to-run form used by runtime backends

Do not use this crate for JIT compilation, native AoT export or load, or `Subject`-based prediction helpers. Those workflows stay in `pharmsol::dsl` in the main `pharmsol` crate.
Do not use this crate for JIT compilation or `Subject`-based prediction helpers. Those workflows stay in `pharmsol::dsl` in the main `pharmsol` crate.

## Main Pipeline

Expand Down Expand Up @@ -51,7 +51,7 @@ The main public modules are:
- `syntax` for the syntax tree
- `diagnostic` for spans, codes, and rendered reports
- `analysis` for the analyzed, fully checked model
- `execution` for the ready-to-run model shared by JIT and AoT backends
- `execution` for the ready-to-run model consumed by the runtime backend

The parser accepts both canonical `model { ... }` source and the authoring
shorthand used by the `pharmsol` examples.
Expand Down
Loading
Loading