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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

151 changes: 107 additions & 44 deletions bin/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ enum Commands {
cycle_budget: Option<u64>,

/// Print the dynamic instruction (cycle) count, plus `Keccak calls` /
/// `Ecsm calls` (accelerator syscall invocations). The accelerator lines
/// are omitted when combined with --flamegraph (that path has no per-log
/// data).
/// `Ecsm calls` / `Dma calls` (accelerator syscall invocations). The
/// accelerator lines are omitted when combined with --flamegraph (that
/// path has no per-log data).
#[arg(long)]
cycles: bool,
},
Expand Down Expand Up @@ -359,6 +359,26 @@ struct FlamegraphCliOptions {
checkpoint_cycles: Option<u64>,
}

/// One tally per [`Accelerator`] variant, printed by `execute --cycles`.
#[derive(Default)]
struct AccelCounts {
keccak: u64,
ecsm: u64,
dma: u64,
}

impl AccelCounts {
/// Exhaustive `match`: a new `Accelerator` variant is a compile error here,
/// so it cannot be executed without also being reported.
fn tally(&mut self, accelerator: Accelerator) {
match accelerator {
Accelerator::Keccak => self.keccak += 1,
Accelerator::Ecsm => self.ecsm += 1,
Accelerator::Dma => self.dma += 1,
}
}
}

/// Classifies one executed instruction as an accelerator syscall invocation.
///
/// Delegates to the executor's canonical `SyscallNumbers::accelerator()` so the
Expand Down Expand Up @@ -412,7 +432,7 @@ fn cmd_execute(
// below (the flamegraph path drives execution inside the executor and does
// not expose per-log data). `None` means "not counted", so the accel lines
// are omitted rather than printed as misleading zeros.
let mut accel_counts: Option<(u64, u64)> = None;
let mut accel_counts: Option<AccelCounts> = None;

let cycle_count = if let Some(ref output_path) = flamegraph.path {
// Shared execute+flamegraph path (executor::flamegraph) instead of
Expand Down Expand Up @@ -478,8 +498,7 @@ fn cmd_execute(
};

let mut cycle_count: u64 = 0;
let mut keccak_calls: u64 = 0;
let mut ecsm_calls: u64 = 0;
let mut counts = AccelCounts::default();
// Reused per chunk: `(current_pc, a7)` for logs whose a7 matches an
// accelerator syscall number. This is a cheap superset — a non-ECALL
// instruction can hold the same value in src1 — that `accelerator_of`
Expand Down Expand Up @@ -509,10 +528,8 @@ fn cmd_execute(
// `logs` is no longer used, so the executor's `&mut` borrow is free
// and the instruction cache can be read to confirm each candidate.
for (pc, a7) in accel_candidates.drain(..) {
match accelerator_of(executor.instructions.get(pc), a7) {
Some(Accelerator::Keccak) => keccak_calls += 1,
Some(Accelerator::Ecsm) => ecsm_calls += 1,
None => {}
if let Some(accelerator) = accelerator_of(executor.instructions.get(pc), a7) {
counts.tally(accelerator);
}
}
if cycle_budget.is_some_and(|budget| cycle_count >= budget) {
Expand All @@ -526,16 +543,17 @@ fn cmd_execute(
}

if cycles {
accel_counts = Some((keccak_calls, ecsm_calls));
accel_counts = Some(counts);
}
cycle_count
};

if cycles {
println!("Cycles: {}", cycle_count);
if let Some((keccak_calls, ecsm_calls)) = accel_counts {
println!("Keccak calls: {}", keccak_calls);
println!("Ecsm calls: {}", ecsm_calls);
if let Some(counts) = accel_counts {
println!("Keccak calls: {}", counts.keccak);
println!("Ecsm calls: {}", counts.ecsm);
println!("Dma calls: {}", counts.dma);
}
}

Expand Down Expand Up @@ -1102,43 +1120,88 @@ mod tests {
assert_eq!(continuation_epoch_size(20).unwrap(), 1 << 20);
}

/// The chip each syscall must drive, written out here rather than read back
/// from `SyscallNumbers::accelerator()`. Comparing the CLI against the
/// executor alone would pass if both agreed on the wrong answer — a chip
/// demoted to `None` has to fail somewhere, and this is that somewhere.
///
/// Cross-checked row by row against `SyscallNumbers::ALL`, which the
/// executor's macro generates from the enum, so a new syscall fails the test
/// until it gets a row here.
const EXPECTED_ACCELERATORS: &[(SyscallNumbers, Option<Accelerator>)] = &[
(SyscallNumbers::KeccakPermute, Some(Accelerator::Keccak)),
(SyscallNumbers::Ecsm, Some(Accelerator::Ecsm)),
(SyscallNumbers::DmaMemcpy, Some(Accelerator::Dma)),
(SyscallNumbers::Print, None),
(SyscallNumbers::Panic, None),
(SyscallNumbers::Commit, None),
(SyscallNumbers::Halt, None),
// `hint` drives its own HINT table, but the executor maps it to no
// `Accelerator`: the ecall adds no correctness constraint, so there is no
// accelerated work to attribute. `execute --cycles` reports no hint line.
(SyscallNumbers::Hint, None),
];

// `accelerator_of` must match the prover's `CpuOperation::from_log`: count an
// invocation only when the instruction is an ECALL AND a7 is the accelerator
// syscall number. Covers both accelerators, the non-accelerator syscalls, a
// non-ECALL whose src1 collides with an accelerator number, and a cache miss.
// syscall number.
#[test]
fn accelerator_of_mirrors_prover_classification() {
use executor::vm::instruction::execution::{ECSM_SYSCALL_NUMBER, KECCAK_SYSCALL_NUMBER};

let ecall = Instruction::EcallEbreak;

assert_eq!(
accelerator_of(Some(&ecall), KECCAK_SYSCALL_NUMBER),
Some(Accelerator::Keccak)
);
assert_eq!(
accelerator_of(Some(&ecall), ECSM_SYSCALL_NUMBER),
Some(Accelerator::Ecsm)
);

// Non-accelerator syscalls (Commit=64, Halt=93) count as neither.
assert_eq!(
accelerator_of(Some(&ecall), SyscallNumbers::Commit as u64),
None
);
assert_eq!(
accelerator_of(Some(&ecall), SyscallNumbers::Halt as u64),
None
);
for &syscall in SyscallNumbers::ALL {
let rows = EXPECTED_ACCELERATORS
.iter()
.filter(|(listed, _)| *listed == syscall)
.count();
assert_eq!(
rows, 1,
"{syscall:?} needs exactly one row in EXPECTED_ACCELERATORS"
);
}

// A non-ECALL instruction whose src1 happens to equal an accelerator a7
// must not count — this is the `f.ecall &&` guard the prover applies.
assert_eq!(
accelerator_of(Some(&Instruction::Fence), KECCAK_SYSCALL_NUMBER),
None
);
for &(syscall, expected) in EXPECTED_ACCELERATORS {
assert_eq!(
accelerator_of(Some(&ecall), syscall.raw()),
expected,
"ECALL with a7 of {syscall:?} must classify as {expected:?}"
);
// A non-ECALL instruction whose src1 happens to equal a syscall a7
// must not count — this is the `f.ecall &&` guard the prover applies.
assert_eq!(
accelerator_of(Some(&Instruction::Fence), syscall.raw()),
None,
"non-ECALL with a7 of {syscall:?} must not count"
);
// No decoded instruction at the pc (cache miss) counts as neither.
assert_eq!(accelerator_of(None, syscall.raw()), None);
}
}

// No decoded instruction at the pc (cache miss) counts as neither.
assert_eq!(accelerator_of(None, KECCAK_SYSCALL_NUMBER), None);
// Every tallied accelerator gets its own counter: no two variants may share
// a field, and each must land in the one the report prints.
#[test]
fn accel_counts_tallies_each_accelerator_separately() {
for &(_, expected_accelerator) in EXPECTED_ACCELERATORS {
let Some(accelerator) = expected_accelerator else {
continue;
};
let mut counts = AccelCounts::default();
counts.tally(accelerator);
assert_eq!(
counts.keccak + counts.ecsm + counts.dma,
1,
"{accelerator:?} must increment exactly one counter"
);
let expected = match accelerator {
Accelerator::Keccak => counts.keccak,
Accelerator::Ecsm => counts.ecsm,
Accelerator::Dma => counts.dma,
};
assert_eq!(
expected, 1,
"{accelerator:?} must increment its own counter"
);
}
}
}
1 change: 1 addition & 0 deletions executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ecsm = { path = "../crypto/ecsm" }
k256 = { version = "0.13", default-features = false, features = ["arithmetic", "expose-field"] }

[dev-dependencies]
proptest = "1.9"
# Test-only: the guest-side syscall crate re-declares the `hint` selectors as `usize`
# and they must stay equal to the `u64` copies here (see `hint_selectors_match_the_guest`).
# Unlike `crypto/crypto`'s and `ethrex-crypto`'s copies of this dep, it is NOT
Expand Down
9 changes: 9 additions & 0 deletions executor/programs/rust/dma_memcpy_cases/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[target.riscv64im-lambda-vm-elf]
rustflags = [
"--cfg", "getrandom_backend=\"custom\"",
"-C", "passes=lower-atomic"
]

[env]
CC_riscv64im_lambda_vm_elf = "clang"
CFLAGS_riscv64im_lambda_vm_elf = "--target=riscv64 -march=rv64im -mabi=lp64 --sysroot=/opt/lambda-vm-sysroot"
Loading
Loading