Skip to content

Parallel evaluator - #48

Merged
jenni-mori1 merged 7 commits into
mainfrom
parallel-eval
Aug 7, 2026
Merged

Parallel evaluator#48
jenni-mori1 merged 7 commits into
mainfrom
parallel-eval

Conversation

@jenni-mori1

Copy link
Copy Markdown
Contributor

This PR adds initial support for parallel Fix evaluation using persistent worker threads and a shared scheduler.

Changes

  • Adds a scheduler with persistent worker threads
  • Uses a shared work queue to distribute evaluation tasks
  • Allows workers to recursively evaluate independent child expressions
  • Adds synchronization for task completion and result collection
  • Adds larger addblob workloads for testing parallel evaluation

@Akshay-Srivatsan

Copy link
Copy Markdown
Contributor

Do you have approximate performance numbers? e.g., time taken on 1 core vs. 16 cores

@jenni-mori1

jenni-mori1 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Do you have approximate performance numbers? e.g., time taken on 1 core vs. 16 cores

I've been timing the work on larger workloads and its 6.2× speedup with eight task executors and 9.4x speedup with 16 task executors.

The workload contains 128 expensive slowaddblob operations arranged into eight independent subtrees, along with eight expensive eight-input additions and a final fast eight-input reduction. My results were:

Serial: 10.53 seconds
7 workers + helping main thread: 1.70 seconds, or 6.2x speedup
15 workers + helping main thread: 1.12 seconds, or 9.4x speedup

I removed the println! statements from both the serial and parallel evaluators so that they don't affect timing and I also changed the parallel evaluator so that application trees with only two arguments are evaluated serially, avoiding the overhead of creating parallel tasks for very small trees. Are there any optimizations you might suggest to make it even faster?

@jenni-mori1

Copy link
Copy Markdown
Contributor Author

For next steps, I’m considering:

  • adding per-worker local queues and work stealing,
  • creating a condition variable for idle workers (is there existing work or a blocking/wakeup primitive in Arca that I could build on? I haven’t found a suitable Rust primitive that works in the current no_std environment.)
  • designing the API needed to eventually move the scheduler into the user program.

I’d appreciate any high-level feedback on which of these would be the most useful next step to prioritize!

@Akshay-Srivatsan Akshay-Srivatsan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start! There's a few cleanup things to simplify the PR, and then I think this'll be ready to merge.

For next steps, I think any of your three ideas is viable/we will eventually need all three. I'd probably lean towards either doing the condition variable (which should be quick hopefully?) or designing the API (which is more involved/conceptual), depending on which makes more sense for your poster/what you want to do.

Comment thread fix/src/parallel_evaluator.rs Outdated
Comment on lines +2 to +24
use alloc::sync::Arc;
//use core::sync::atomic::{AtomicBool, Ordering};
use kernel::{coreid, kthread};
//use kernel::kthread::{KMutex, yield_now};
//use kernel::tsc;
use crate::scheduler::{Scheduler, Task};

use crate::handle::*;
use crate::runtime::Runtime;
use crate::storage::Storage;
use kernel::prelude::*;

// use fixhandle::rawhandle::{Encode, Handle, Object, Ref, Thunk, TreeName};

// use fixruntime::{
// common::CouponTrades,
// fixruntime::{FixRuntime, FixTreeData},
// runtime::{DeterministicEquivRuntime, Executor},
// storage::FixData,
// };

// use common::bitpack::BitPack;
// use kernel::prelude::*;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the unused imports here?

let result = self.eval_test(work.get_handle(), EvalType::Parallel);
work.task_complete(result)
} else {
//change this to condition variable, so it does no busy waiting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will most likely have to implement a condition variable using atomics, which I think should be a separate follow-up PR.

Comment thread fix/src/parallel_evaluator.rs Outdated
Comment on lines +92 to +96
/*
pub fn runtime(&self) -> &R {
&self.runtime
}
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed?

Comment thread fix/src/parallel_evaluator.rs Outdated
Comment on lines +208 to +249
/*
fn eval_tree_seq(&self, handle: Tree) -> Tree {
let start = tsc::read_cycles();

println!(
"[timing] eval_tree_seq start on core {}: {:?}",
kernel::coreid(),
handle
);

let tree = self.runtime.storage().get_tree(handle).unwrap();

let evaled: Vec<Handle> = tree
.as_ref()
.iter()
.copied()
.map(|x| {
let child_start = tsc::read_cycles();
let result = self.eval(x);

println!(
"[timing] seq child {:?} -> {:?}, cycles={}",
x,
result,
cycles_since(child_start)
);

result
})
.collect();

let result_tree = self.runtime.storage().add_tree(&evaled);

println!(
"[timing] eval_tree_seq done: {:?}, total_cycles={}",
handle,
cycles_since(start)
);

result_tree
}
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Comment thread fix/src/main.rs
Comment on lines +132 to +217
// Jennifer: tons of redundancy but I just didn't want to change original code,
// in case errors showed up
// the main change is just calling the parallel evaluator and how its passed in
fn eval_file_parallel(filename: &str) {
let mut file = File::open(filename, true, false, false, false, false).unwrap();
let len = file.seek(Whence::End(0)) as usize;
file.seek(Whence::Start(0));
let mut buf = vec![0; len];
file.read_exact(&mut buf);

let file = core::str::from_utf8(&buf).unwrap();

let lexer = Lexer::new(&file);
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(&tokens);
let program = parser.parse_program().unwrap();

let runtime = FixOnArca::default();
let evaluator = parallel_evaluator::Evaluator::new(runtime);

let mut context = BTreeMap::new();
for statement in program {
match statement {
Statement::Assign { name, expr } => {
let result = eval_parallel(evaluator.as_ref(), &expr, &mut context);
context.insert(name, result);
}
Statement::Print(expr) | Statement::Expr(expr) => {
let x = eval_parallel(evaluator.as_ref(), &expr, &mut context);
println!("handle: {x}");
if let Handle::Object(Object::Blob(blob)) = x {
let contents = evaluator.storage().get_blob(blob).unwrap();
println!("result is a Blob: {contents:?}");
if contents.len() == 8 {
let bytes: [u8; 8] = (*contents).try_into().unwrap();
let value = u64::from_le_bytes(bytes);
println!("\tas a u64: {value}");
}
}
}
}
}
}

fn eval_parallel(
evaluator: &parallel_evaluator::Evaluator<FixOnArca>,
e: &Expr,
ctx: &mut BTreeMap<String, Handle>,
) -> Handle {
match e {
Expr::Identifier(x) => *ctx.get(x).expect("undefined identifier"),
Expr::Number(x) => {
let bytes = i64::to_le_bytes(*x);
evaluator.storage().add_blob(&bytes).into()
}
Expr::String(x) => {
let bytes = x.as_bytes();
evaluator.storage().add_blob(bytes).into()
}
Expr::Call { name, args } => {
let arg_handles: Vec<Handle> = args
.iter()
.map(|x| eval_parallel(evaluator, x, ctx))
.collect();
match name.as_str() {
"create_blob" if let Expr::String(path) = &args.get(0).expect("no path") => {
let mut file = File::open(path, true, false, false, false, false).unwrap();
let len = file.seek(Whence::End(0));
file.seek(Whence::Start(0));
let mut buf = vec![0; len as usize];
file.read_exact(&mut buf);
core::mem::forget(file);
evaluator.storage().add_blob(&buf).into()
}
"create_tree" => evaluator.storage().add_tree(&arg_handles).into(),
"create_application_thunk" => {
Thunk::Application(arg_handles[0].unwrap_object().unwrap_tree()).into()
}
"create_strict_encode" => Encode::Strict(arg_handles[0].unwrap_thunk()).into(),
"eval" => evaluator.eval(arg_handles[0]),
name => todo!("call {name} {args:?}"),
}
}
Expr::Group(x) => eval_parallel(evaluator, x, ctx),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to rebase this on top of Haibib's changes, which I think simplify this code?

@Haibib thoughts?

@jenni-mori1 jenni-mori1 Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll wait to rebase until Habib's new PR is approved and I'll push again!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we necessarily need a separate serial and parallel evaluator? If we have them we should probably try to deduplicate the code between them, but I think we'll eventually only want the parallel one so we might we well just replace the serial one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is my fault -- I had thought that (especially when we move the evaluators to user-space) we probably want to keep a simple single-threaded evaluator around so we can run it for simplicity and debugging and have for teaching/explanatory purposes...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I’ll keep the serial and parallel evaluators separate, but let me know if you’d prefer that I merge them instead.

@Akshay-Srivatsan Akshay-Srivatsan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once you rebase/test that things still work, this is good to merge. (Or you could try to race Haibib to merge first and let him rebase, I guess...)

Comment thread addblob_extended.fix Outdated
o = 15;
p = 16;

add = create_blob("./target/x86_64-unknown-none/slowaddblob");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, I had to cargo clean and rebuild before it found this file.

@jenni-mori1
jenni-mori1 merged commit 60b120e into main Aug 7, 2026
6 checks passed
@jenni-mori1
jenni-mori1 deleted the parallel-eval branch August 7, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants