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
2 changes: 1 addition & 1 deletion bin/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ cargo run -p cli --release -- execute <PROGRAM.elf> [--private-input <FILE>] [--
|---|---|
| `--private-input <FILE>` | Pass private input bytes to the guest (read via `get_private_input()`). |
| `--flamegraph <FILE>` | Generate folded-stack flamegraph output. See [Guest Program Flamegraphs](#guest-program-flamegraphs). |
| `--cycles` | Count instructions during execution and print the dynamic instruction count. Also reports `Keccak calls` / `Ecsm calls` (accelerator syscall invocations). Combined with `--flamegraph`, the accelerator lines are omitted (the flamegraph path exposes no per-log data). |
| `--cycles` | Count instructions during execution and print the dynamic instruction count. Also reports `Keccak calls` / `Ecsm calls` / `Dma calls` (accelerator syscall invocations), plus `Dma bytes` copied and the `Dma rows` those copies add to the trace before its power-of-two padding. One guest `memcpy` is chunked into several DMA ecalls, so the byte and row lines, not the call count, are what the copies cost. Combined with `--flamegraph`, the accelerator lines are omitted (the flamegraph path exposes no per-log data). |

### Prove

Expand Down
69 changes: 55 additions & 14 deletions bin/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use clap::{Parser, Subcommand, ValueHint};
#[global_allocator]
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
use executor::vm::instruction::decoding::Instruction;
use executor::vm::instruction::execution::{Accelerator, SyscallNumbers};
use executor::vm::instruction::execution::{Accelerator, SyscallNumbers, dma_memcpy_trace_rows};
use executor::{elf::Elf, flamegraph::FlamegraphGenerator, vm::execution::Executor};
use prover::VmProof;
use stark::proof::options::GoldilocksCubicProofOptions;
Expand Down Expand Up @@ -142,9 +142,12 @@ enum Commands {
cycle_budget: Option<u64>,

/// Print the dynamic instruction (cycle) count, plus `Keccak calls` /
/// `Ecsm calls` / `Dma 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), and for
/// DMA the `Dma bytes` copied and the `Dma rows` those copies add to the
/// trace before its power-of-two padding. One `memcpy` is chunked into
/// several DMA ecalls, so the byte and row lines, not the call count, are
/// what the copies cost. The accelerator lines are omitted when combined
/// with --flamegraph (that path has no per-log data).
#[arg(long)]
cycles: bool,
},
Expand Down Expand Up @@ -365,16 +368,28 @@ struct AccelCounts {
keccak: u64,
ecsm: u64,
dma: u64,
/// Bytes copied and DMA table rows those copies consume. Keccak and ECSM
/// cost the same per call, so DMA is the only accelerator whose report needs
/// a size next to its count: one `memcpy` becomes as many ecalls as the
/// guest stub chunks it into, which makes `dma` alone a poor cost proxy.
dma_bytes: u64,
dma_rows: 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) {
/// so it cannot be executed without also being reported. `dst_val` is the
/// ECALL's logged destination operand, which for DMA is the chunk's byte
/// count and for the other accelerators is unused.
fn tally(&mut self, accelerator: Accelerator, dst_val: u64) {
match accelerator {
Accelerator::Keccak => self.keccak += 1,
Accelerator::Ecsm => self.ecsm += 1,
Accelerator::Dma => self.dma += 1,
Accelerator::Dma => {
self.dma += 1;
self.dma_bytes += dst_val;
self.dma_rows += dma_memcpy_trace_rows(dst_val);
}
}
}
}
Expand Down Expand Up @@ -499,12 +514,12 @@ fn cmd_execute(

let mut cycle_count: 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
// Reused per chunk: `(current_pc, a7, dst_val)` 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`
// confirms below, once the chunk's `&Log` borrow (tied to the executor's
// `&mut`) is released so the instruction cache can be read again.
let mut accel_candidates: Vec<(u64, u64)> = Vec::new();
let mut accel_candidates: Vec<(u64, u64, u64)> = Vec::new();
loop {
let logs = match executor.resume_budgeted(cycle_count, cycle_budget) {
Ok(logs) => logs,
Expand All @@ -521,15 +536,15 @@ fn cmd_execute(
.map(|s| s.accelerator().is_some())
.unwrap_or(false)
{
accel_candidates.push((log.current_pc, log.src1_val));
accel_candidates.push((log.current_pc, log.src1_val, log.dst_val));
}
}
}
// `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(..) {
for (pc, a7, dst_val) in accel_candidates.drain(..) {
if let Some(accelerator) = accelerator_of(executor.instructions.get(pc), a7) {
counts.tally(accelerator);
counts.tally(accelerator, dst_val);
}
}
if cycle_budget.is_some_and(|budget| cycle_count >= budget) {
Expand All @@ -554,6 +569,8 @@ fn cmd_execute(
println!("Keccak calls: {}", counts.keccak);
println!("Ecsm calls: {}", counts.ecsm);
println!("Dma calls: {}", counts.dma);
println!("Dma bytes: {}", counts.dma_bytes);
println!("Dma rows: {}", counts.dma_rows);
}
}

Expand Down Expand Up @@ -1187,7 +1204,7 @@ mod tests {
continue;
};
let mut counts = AccelCounts::default();
counts.tally(accelerator);
counts.tally(accelerator, 0);
assert_eq!(
counts.keccak + counts.ecsm + counts.dma,
1,
Expand All @@ -1204,4 +1221,28 @@ mod tests {
);
}
}

// The byte and row lines are what make the DMA report a cost figure rather
// than a call count, so they must accumulate across chunked ecalls and use
// the executor's row formula — the same one trace generation sizes with.
#[test]
fn accel_counts_sizes_dma_calls() {
let mut counts = AccelCounts::default();
for bytes in [256, 256, 8, 3, 0] {
counts.tally(Accelerator::Dma, bytes);
}

assert_eq!(counts.dma, 5, "every DMA ecall counts as one call");
assert_eq!(counts.dma_bytes, 523);
// 33 + 33 + 2 + 4 + 1: eight-byte rows, one row per tail byte, and a
// terminal row each, with the zero-byte ecall contributing only its
// terminal row.
assert_eq!(counts.dma_rows, 73);

// The other accelerators must leave the DMA size lines alone.
let mut others = AccelCounts::default();
others.tally(Accelerator::Keccak, 200);
others.tally(Accelerator::Ecsm, 32);
assert_eq!((others.dma, others.dma_bytes, others.dma_rows), (0, 0, 0));
}
}
12 changes: 12 additions & 0 deletions docs/general_flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@ The Lambda VM proves correct execution of a RISC-V (RV64IM) program against an i
4. **Proof system** ([`crypto/stark/`](../crypto/stark/)) — commits to each table's trace via Merkle trees, samples challenges via Fiat-Shamir, and runs FRI for the low-degree test. Produces a `MultiProof`; the verifier replays the transcript and checks all AIR and lookup constraints.

For a deeper dive into each component see the [proof system overview](./cryptography/proof_system.md).

## Accelerated memory operations

`memcpy` is accelerated: `lambda-vm-syscalls` exports it under its standard, unmangled C name, so both explicit calls and the copies the compiler emits implicitly (struct moves, slice copies, `Vec` growth) reach the DMA ecall with no guest source changes. Behaviour is identical to the C function for every input, including `n == 0` and any alignment of `dest`, `src` or `n`. `memmove`, `memset` and `memcmp` are not accelerated and fall back to the toolchain's `compiler-builtins` definitions.

**Observability.** `cli execute <elf> --cycles` reports `Dma calls`, `Dma bytes` and `Dma rows`. The call line confirms the accelerator is engaged at all; the byte and row lines are the cost, since the guest stub chunks one `memcpy` into as many ecalls as it needs and each ecall adds one DMA row per eight-byte chunk, one per tail byte, and a terminal row. `Dma rows` is the raw row count the copies contribute, before the DMA trace is padded to a power-of-two height — for a guest with few copies the padded table is larger than the reported figure.

There is no aligned/misaligned split to report: the DMA chunk width is chosen from the bytes remaining, not from the alignment of `dest` or `src`, so a misaligned copy costs exactly what an aligned copy of the same length costs and there is no fast path to distinguish.

**Symbol resolution.** `compiler-builtins` defines `memcpy` *weakly*, and a linker extracts a static-archive member only to satisfy an *undefined* symbol — a weak definition already satisfies the reference, so a strong definition that lives in a member nothing else pulls in is dropped silently, with no duplicate-symbol diagnostic. Lambda VM therefore defines `memcpy` in [`syscalls/src/entrypoint.rs`](../syscalls/src/entrypoint.rs), the same object that defines `_start`, which every guest links unconditionally. That object is always extracted, so the strong definition is in the link graph from the start and overrides the weak one. No `--whole-archive` and no guest link flag is required, and resolution does not depend on archive order.

Placing `memcpy` beside `_start` is insurance rather than a repair: defined in `syscalls.rs` it also resolved correctly in practice, because rustc merged that module into a codegen unit every guest already pulled in for `commit` and `sys_halt`. That is luck, not a guarantee — it depends on codegen-unit merging and on the guest referencing some other symbol from the same module. Co-locating with `_start` removes both dependencies, and `test_dma_memcpy_compiler_emitted_copies` (a guest that never names `memcpy`, asserting the DMA ecall count stays above zero) is what detects a regression, since a guest that falls back to the weak definition still produces correct output.
9 changes: 9 additions & 0 deletions executor/programs/rust/dma_memcpy_implicit/.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