Skip to content

Commit 579ab4f

Browse files
authored
perf(vdf): optimize hot loop with direct compress256 and copy elimination (#1214)
* perf(vdf): replace Sha256 API with direct compress256 in hot loop * perf(vdf): eliminate per-iteration 32-byte copy in hot loop * refactor(vdf): review comments * refactor(vdf): review comments * refactor(vdf): review comments * chore(ci): restrict benchmark workflow to PR creation, approval, and merge * fix(bench): update vdf_bench to match rebased vdf_sha signature * fix(vdf): add debug_assert for checkpoint length in vdf_sha * fix(ci): handle workflow_dispatch branch detection in bench workflow * refactor(vdf): address review findings for vdf optimisation PR * refactor(chain-tests): remove unused vdf imports * refactor(vdf): extract compress_n_rounds and remove redundant comments * fix(vdf): qualify size_of/align_of for Rust 2024 edition * feat(ci): seed branch benchmarks with master baseline
1 parent bc0dd4c commit 579ab4f

File tree

9 files changed

+252
-261
lines changed

9 files changed

+252
-261
lines changed

.github/workflows/bench.yml

Lines changed: 56 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ concurrency:
99

1010
on:
1111
pull_request:
12-
types: [opened, synchronize, closed]
12+
types: [opened, closed]
1313
pull_request_review:
1414
types: [submitted]
1515
push:
@@ -29,106 +29,36 @@ jobs:
2929
outputs:
3030
should-run: ${{ steps.set-output.outputs.should-run }}
3131
steps:
32-
- uses: actions/checkout@v4
33-
with:
34-
fetch-depth: 0
35-
- name: Check if CI should run
32+
- name: Check if benchmarks should run
3633
id: set-output
3734
env:
38-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
PR_DISABLE_CI: ${{ contains(github.event.pull_request.body, '#disable-ci') }}
36+
COMMIT_DISABLE_CI: ${{ contains(github.event.head_commit.message, '#disable-ci') }}
3937
run: |
40-
# default to running
41-
SHOULD_RUN=true
42-
43-
# this string disables CI if a commit/branch description includes it
44-
DISABLE_CI_STRING="#disable-ci"
45-
46-
# check if CI is disabled in the commit message (for any event type)
47-
if [[ "${{ contains(github.event.head_commit.message, '#disable-ci') }}" == "true" ]]; then
48-
echo "commit message contains '$DISABLE_CI_STRING', skipping CI..."
49-
SHOULD_RUN=false
50-
fi
38+
SHOULD_RUN=false
5139
52-
# Function to check if PR description contains disable string
53-
check_pr_description() {
54-
local pr_number=$1
55-
if [[ -n "$pr_number" ]]; then
56-
PR_DESCRIPTION=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
57-
"https://api.github.com/repos/${{ github.repository }}/pulls/$pr_number" | \
58-
jq --raw-output .body)
59-
60-
if [[ "$PR_DESCRIPTION" == *"$DISABLE_CI_STRING"* ]]; then
61-
echo "PR #$pr_number description contains '$DISABLE_CI_STRING'"
62-
return 1
63-
fi
64-
fi
65-
return 0
66-
}
67-
68-
# For push events, check if this commit is part of an open PR
69-
if [[ "${{ github.event_name }}" == "push" && "$SHOULD_RUN" == "true" ]]; then
70-
COMMIT_SHA="${{ github.event.after }}"
71-
ASSOCIATED_PRS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
72-
"https://api.github.com/repos/${{ github.repository }}/commits/$COMMIT_SHA/pulls" | \
73-
jq --raw-output '.[].number')
74-
75-
if [[ -n "$ASSOCIATED_PRS" ]]; then
76-
echo "Found PRs associated with this commit: $ASSOCIATED_PRS"
77-
78-
ALL_PRS_DISABLED=true
79-
PR_COUNT=0
80-
81-
for PR_NUMBER in $ASSOCIATED_PRS; do
82-
PR_COUNT=$((PR_COUNT+1))
83-
if check_pr_description "$PR_NUMBER"; then
84-
echo "PR #$PR_NUMBER doesn't have the disable string, will run CI"
85-
ALL_PRS_DISABLED=false
86-
fi
87-
done
88-
89-
if [[ "$ALL_PRS_DISABLED" == "true" && "$PR_COUNT" -gt 0 ]]; then
90-
echo "All $PR_COUNT associated PRs have the disable string, skipping CI"
91-
SHOULD_RUN=false
92-
fi
93-
else
94-
echo "No open PRs found associated with this commit"
95-
fi
96-
fi
97-
98-
# For pull_request events, check PR description
99-
if [[ "${{ github.event_name }}" == "pull_request" && "$SHOULD_RUN" == "true" ]]; then
100-
PR_NUMBER="${{ github.event.pull_request.number }}"
101-
if ! check_pr_description "$PR_NUMBER"; then
102-
SHOULD_RUN=false
103-
fi
40+
if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.action }}" == "opened" ]]; then
41+
SHOULD_RUN=true
10442
fi
10543
106-
# For pull_request_review events, only run on approvals; check PR description
107-
if [[ "${{ github.event_name }}" == "pull_request_review" && "$SHOULD_RUN" == "true" ]]; then
108-
if [[ "${{ github.event.review.state }}" != "approved" ]]; then
109-
echo "Review is not an approval, skipping..."
110-
SHOULD_RUN=false
111-
fi
112-
113-
if [[ "$SHOULD_RUN" == "true" ]]; then
114-
PR_NUMBER="${{ github.event.pull_request.number }}"
115-
if ! check_pr_description "$PR_NUMBER"; then
116-
SHOULD_RUN=false
117-
fi
118-
fi
44+
if [[ "${{ github.event_name }}" == "pull_request_review" && "${{ github.event.review.state }}" == "approved" ]]; then
45+
SHOULD_RUN=true
11946
fi
12047
121-
# always run on the default branch
122-
if [[ "${{ github.ref }}" == "refs/heads/${{ github.event.repository.default_branch }}" ]]; then
48+
if [[ "${{ github.event_name }}" == "push" ]]; then
12349
SHOULD_RUN=true
12450
fi
12551
126-
# always run on workflow_dispatch
12752
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
12853
SHOULD_RUN=true
12954
fi
13055
131-
echo "Should run? ${SHOULD_RUN}"
56+
if [[ "$PR_DISABLE_CI" == "true" || "$COMMIT_DISABLE_CI" == "true" ]]; then
57+
echo "#disable-ci found, skipping benchmarks"
58+
SHOULD_RUN=false
59+
fi
60+
61+
echo "Should run benchmarks? ${SHOULD_RUN}"
13262
echo "should-run=${SHOULD_RUN}" >> $GITHUB_OUTPUT
13363
13464
cleanup-branch-data:
@@ -200,28 +130,64 @@ jobs:
200130
print(f'Converted {len(results)} benchmarks from ns to ms')
201131
"
202132
133+
- name: Seed branch with master baseline
134+
if: (github.head_ref || github.ref_name) != github.event.repository.default_branch
135+
env:
136+
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
137+
run: |
138+
git fetch origin gh-pages:gh-pages
139+
git worktree add /tmp/gh-pages gh-pages
140+
141+
BRANCH_DIR="/tmp/gh-pages/dev/bench/$BRANCH_NAME"
142+
MASTER_DATA="/tmp/gh-pages/data.js"
143+
144+
if [ ! -f "$BRANCH_DIR/data.js" ] && [ -f "$MASTER_DATA" ]; then
145+
mkdir -p "$BRANCH_DIR"
146+
cp "$MASTER_DATA" "$BRANCH_DIR/data.js"
147+
cd /tmp/gh-pages
148+
git config user.name "github-actions[bot]"
149+
git config user.email "github-actions[bot]@users.noreply.github.com"
150+
git add "dev/bench/$BRANCH_NAME/data.js"
151+
git commit -m "seed: copy master baseline to branch $BRANCH_NAME"
152+
git push origin gh-pages
153+
else
154+
echo "Branch data already exists or no master baseline found, skipping seed"
155+
fi
156+
157+
git worktree remove /tmp/gh-pages
158+
203159
- name: Store benchmark result
204160
uses: benchmark-action/github-action-benchmark@v1
205161
with:
206162
tool: 'customSmallerIsBetter'
207163
output-file-path: bench-output.json
208164
github-token: ${{ secrets.GITHUB_TOKEN }}
209165
auto-push: true
210-
benchmark-data-dir-path: ${{ github.head_ref && format('dev/bench/{0}', github.head_ref) || '.' }}
166+
benchmark-data-dir-path: ${{ (github.head_ref || github.ref_name) != github.event.repository.default_branch && format('dev/bench/{0}', github.head_ref || github.ref_name) || '.' }}
211167
alert-threshold: '120%'
212168
comment-on-alert: true
213169
fail-on-alert: false
214170

215171
- name: Comment benchmark results URL on PR
216-
if: github.head_ref != ''
172+
if: (github.head_ref || github.ref_name) != github.event.repository.default_branch
217173
uses: actions/github-script@v7
218174
with:
219175
script: |
220-
const prNumber = context.payload.pull_request?.number
176+
let prNumber = context.payload.pull_request?.number
221177
?? context.payload.review?.pull_request?.number;
178+
179+
if (!prNumber) {
180+
const branch = '${{ github.head_ref || github.ref_name }}';
181+
const { data: prs } = await github.rest.pulls.list({
182+
...context.repo,
183+
head: `${context.repo.owner}:${branch}`,
184+
state: 'open',
185+
});
186+
prNumber = prs[0]?.number;
187+
}
222188
if (!prNumber) return;
223189
224-
const branch = '${{ github.head_ref }}';
190+
const branch = '${{ github.head_ref || github.ref_name }}';
225191
const url = `https://irys-xyz.github.io/irys/dev/bench/${encodeURIComponent(branch)}/index.html`;
226192
const marker = '<!-- benchmark-results-url -->';
227193
const body = `${marker}\n**Benchmark results:** ${url}`;

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ assert_matches = "1"
8080
bytes = "1.5"
8181
derive_more = { version = "2", features = ["full"] }
8282
eyre = "0.6"
83-
sha2 = "0.10"
83+
sha2 = { version = "0.10", features = ["compress"] }
8484
rayon = "1.8.0"
8585
reqwest = "0.12.15"
8686
color-eyre = "0.6"

crates/chain-tests/src/utils.rs

Lines changed: 15 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ use irys_types::{
6262
use irys_types::{
6363
HandshakeRequest, HandshakeRequestV2, Interval, PartitionChunkOffset, ProtocolVersion,
6464
};
65+
use irys_vdf::compute_step_checkpoints;
6566
use irys_vdf::state::VdfStateReadonly;
66-
use irys_vdf::{step_number_to_salt_number, vdf_sha};
6767
use itertools::Itertools as _;
6868
use reth::{
6969
network::{PeerInfo, Peers as _},
@@ -89,6 +89,7 @@ pub async fn capacity_chunk_solution(
8989
vdf_steps_guard: VdfStateReadonly,
9090
config: &Config,
9191
difficulty: U256,
92+
reset_seed: H256,
9293
) -> SolutionContext {
9394
// Wait until we have at least 2 new VDF steps so we can compute checkpoints for (step-1, step)
9495
let max_wait_retries = 20;
@@ -122,25 +123,8 @@ pub async fn capacity_chunk_solution(
122123
}
123124
};
124125

125-
// Calculate last step checkpoints for current_step - 1
126-
let mut hasher = Sha256::new();
127-
let mut salt = irys_types::U256::from(step_number_to_salt_number(
128-
&config.vdf,
129-
current_step.saturating_sub(1),
130-
));
131-
let mut seed = steps[0];
132-
133-
let mut checkpoints: Vec<H256> =
134-
vec![H256::default(); config.vdf.num_checkpoints_in_vdf_step];
135-
136-
vdf_sha(
137-
&mut hasher,
138-
&mut salt,
139-
&mut seed,
140-
config.vdf.num_checkpoints_in_vdf_step,
141-
config.vdf.num_iterations_per_checkpoint(),
142-
&mut checkpoints,
143-
);
126+
let (_seed, checkpoints) =
127+
compute_step_checkpoints(&config.vdf, current_step, steps[0], reset_seed);
144128

145129
// Determine recall range for this step
146130
let recall_range_idx = block_validation::get_recall_range(
@@ -242,27 +226,7 @@ pub async fn capacity_chunk_solution(
242226
chunk: entropy_chunk,
243227
vdf_step: current_step,
244228
checkpoints: H256List(
245-
// recompute checkpoints for fallback
246-
{
247-
let mut h = Sha256::new();
248-
let mut s = irys_types::U256::from(step_number_to_salt_number(
249-
&config.vdf,
250-
current_step.saturating_sub(1),
251-
));
252-
let mut sd = steps[0];
253-
let mut cps: Vec<H256> =
254-
vec![H256::default(); config.vdf.num_checkpoints_in_vdf_step];
255-
vdf_sha(
256-
&mut h,
257-
&mut s,
258-
&mut sd,
259-
config.vdf.num_checkpoints_in_vdf_step,
260-
config.vdf.num_iterations_per_checkpoint(),
261-
&mut cps,
262-
);
263-
H256List(cps)
264-
}
265-
.0,
229+
compute_step_checkpoints(&config.vdf, current_step, steps[0], reset_seed).1,
266230
),
267231
seed: Seed(steps[1]),
268232
solution_hash,
@@ -3657,21 +3621,15 @@ pub async fn solution_context_with_poa_chunk(
36573621
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
36583622
};
36593623

3660-
// Compute checkpoints for (step-1)
3661-
let mut hasher = Sha256::new();
3662-
let mut salt =
3663-
irys_types::U256::from(step_number_to_salt_number(&node_ctx.config.vdf, step - 1));
3664-
let mut seed = steps[0];
3665-
let mut checkpoints: Vec<H256> =
3666-
vec![H256::default(); node_ctx.config.vdf.num_checkpoints_in_vdf_step];
3667-
vdf_sha(
3668-
&mut hasher,
3669-
&mut salt,
3670-
&mut seed,
3671-
node_ctx.config.vdf.num_checkpoints_in_vdf_step,
3672-
node_ctx.config.vdf.num_iterations_per_checkpoint(),
3673-
&mut checkpoints,
3674-
);
3624+
let reset_seed = {
3625+
let read = node_ctx.block_tree_guard.read();
3626+
let parent_hash = read.get_max_cumulative_difficulty_block().1;
3627+
read.get_block(&parent_hash)
3628+
.map(|b| b.vdf_limiter_info.next_seed)
3629+
.unwrap_or_default()
3630+
};
3631+
let (_seed, checkpoints) =
3632+
compute_step_checkpoints(&node_ctx.config.vdf, step, steps[0], reset_seed);
36753633

36763634
// For deterministic linkage without recall-range dependency, use offset 0
36773635
let partition_hash = H256::zero();
@@ -3729,6 +3687,7 @@ pub async fn solution_context(node_ctx: &IrysNodeCtx) -> Result<SolutionContext,
37293687
vdf_steps_guard.clone(),
37303688
&node_ctx.config,
37313689
prev_block.diff,
3690+
prev_block.vdf_limiter_info.next_seed,
37323691
)
37333692
.await;
37343693
if !was_vdf_enabled {

crates/vdf/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ irys-types = { workspace = true, features = ["test-utils"] }
2626
irys-database = { workspace = true, features = ["test-utils"] }
2727
tracing-subscriber.workspace = true
2828
criterion.workspace = true
29+
proptest.workspace = true
2930

3031
[[bench]]
3132
name = "vdf_bench"

0 commit comments

Comments
 (0)