@@ -492,6 +492,84 @@ void build_constraints_parallel(Builder& builder,
492492 profile_and_collect (constraints.multi_scalar_mul_constraints , msm_handler, msm_key);
493493 profile_and_collect (constraints.ec_add_constraints , ec_add_handler, const_key);
494494
495+ // Recursion constraints are parallelized like other constraint types, but each task also
496+ // captures a HonkRecursionConstraintOutput for post-join merging (needed for pairing point
497+ // propagation and IPA finalization).
498+ struct RecursionTaskInfo {
499+ HonkRecursionConstraintOutput<Builder> output;
500+ bool update_ipa_data = false ;
501+ bool is_root_rollup = false ;
502+ };
503+ size_t num_rec_tasks = constraints.honk_recursion_constraints .size () +
504+ constraints.chonk_recursion_constraints .size () +
505+ constraints.avm_recursion_constraints .size ();
506+ std::vector<RecursionTaskInfo> recursion_task_outputs (num_rec_tasks);
507+ size_t rec_out_idx = 0 ;
508+
509+ // Helper: execute a single honk recursion constraint based on proof_type
510+ auto execute_honk_recursion = [](Builder& b,
511+ const RecursionConstraint& c) -> HonkRecursionConstraintOutput<Builder> {
512+ if (c.proof_type == HONK_ZK) {
513+ return create_honk_recursion_constraints<UltraZKRecursiveFlavor_<Builder>,
514+ stdlib::recursion::honk::DefaultIO<Builder>>(b, c);
515+ } else if (c.proof_type == HONK) {
516+ return create_honk_recursion_constraints<UltraRecursiveFlavor_<Builder>,
517+ stdlib::recursion::honk::DefaultIO<Builder>>(b, c);
518+ } else {
519+ // Rollup IO is only supported on UltraCircuitBuilder
520+ if constexpr (std::is_same_v<Builder, UltraCircuitBuilder>) {
521+ return create_honk_recursion_constraints<UltraRecursiveFlavor_<Builder>,
522+ stdlib::recursion::honk::RollupIO>(b, c);
523+ } else {
524+ bb::assert_failure (" Rollup Honk proof type not supported on MegaBuilder" );
525+ return {};
526+ }
527+ }
528+ };
529+
530+ // Profiling handler (discards output — only used for measuring gate counts)
531+ auto honk_rec_handler = [&execute_honk_recursion](Builder& b, const RecursionConstraint& c) {
532+ execute_honk_recursion (b, c);
533+ };
534+ auto honk_rec_key = [](const RecursionConstraint& c) -> uint32_t { return c.proof_type ; };
535+
536+ // Profile honk recursion constraints by proof_type
537+ std::map<uint32_t , size_t > honk_rec_profiles;
538+ for (size_t i = 0 ; i < constraints.honk_recursion_constraints .size (); i++) {
539+ uint32_t k = honk_rec_key (constraints.honk_recursion_constraints [i]);
540+ if (honk_rec_profiles.count (k) == 0 ) {
541+ auto profile = profile_constraint_type<Builder>(
542+ constraints.honk_recursion_constraints [i], honk_rec_handler, num_witnesses);
543+ honk_rec_profiles[k] = profiles.size ();
544+ profiles.push_back (profile);
545+ }
546+ }
547+ // Add honk recursion tasks in vector order with output capture
548+ for (size_t i = 0 ; i < constraints.honk_recursion_constraints .size (); i++) {
549+ const auto & c = constraints.honk_recursion_constraints [i];
550+ size_t profile_idx = honk_rec_profiles.at (c.proof_type );
551+ const auto & profile = profiles[profile_idx];
552+ auto sizes = profile.block_sizes ;
553+ sizes.num_rom_arrays = profile.num_rom_arrays_per_instance ;
554+ sizes.num_ram_arrays = profile.num_ram_arrays_per_instance ;
555+
556+ size_t out_idx = rec_out_idx++;
557+ recursion_task_outputs[out_idx].update_ipa_data =
558+ (c.proof_type == ROLLUP_HONK || c.proof_type == ROOT_ROLLUP_HONK);
559+ recursion_task_outputs[out_idx].is_root_rollup = (c.proof_type == ROOT_ROLLUP_HONK);
560+
561+ tasks.emplace_back (
562+ [&constraints, i, &execute_honk_recursion, &recursion_task_outputs, out_idx](BaseBuilder& b) {
563+ recursion_task_outputs[out_idx].output =
564+ execute_honk_recursion (static_cast <Builder&>(b), constraints.honk_recursion_constraints [i]);
565+ });
566+ task_sizes.push_back (sizes);
567+ task_profile_indices.push_back (profile_idx);
568+ }
569+
570+ // TODO: Chonk and AVM recursion constraints — same pattern as honk above.
571+ // For now they fall through to Phase 4 sequential processing if present.
572+
495573 // Phase 2: Prepare the builder's caches from profiles (no constraint execution).
496574 prepare_builder_from_profiles (builder, profiles);
497575
@@ -511,32 +589,61 @@ void build_constraints_parallel(Builder& builder,
511589 }
512590 }
513591
514- // Phase 3: Execute ALL instances in parallel
515- // execute_parallel will set up per-thread ROM/RAM cursors using the num_rom/ram_arrays in task_sizes
592+ // Phase 3: Execute ALL instances in parallel (including recursion constraints)
516593 if (!tasks.empty ()) {
517594 builder.execute_parallel (tasks, task_sizes, num_threads);
518595 }
519596
520- // Phase 4: Block constraints and recursion constraints are processed sequentially .
597+ // Phase 4: Block constraints (sequential — these reference variables from earlier constraints) .
521598 for (const auto & [constraint, opcode_indices] :
522599 zip_view (constraints.block_constraints , constraints.original_opcode_indices .block_constraints )) {
523600 create_block_constraints (builder, constraint);
524601 }
525602
526- const bool is_hn_recursion_constraints = !constraints.hn_recursion_constraints .empty ();
527- GateCounter gate_counter{ &builder, false };
528- std::vector<size_t > dummy_gates_per_opcode;
529- HonkRecursionConstraintsOutput<Builder> output = create_recursion_constraints<Builder>(
530- builder,
531- gate_counter,
532- dummy_gates_per_opcode,
533- metadata.ivc ,
534- { constraints.honk_recursion_constraints , constraints.original_opcode_indices .honk_recursion_constraints },
535- { constraints.avm_recursion_constraints , constraints.original_opcode_indices .avm_recursion_constraints },
536- { constraints.hn_recursion_constraints , constraints.original_opcode_indices .hn_recursion_constraints },
537- { constraints.chonk_recursion_constraints , constraints.original_opcode_indices .chonk_recursion_constraints });
603+ // Phase 4b: Merge recursion outputs from parallel tasks and process remaining sequential recursion.
604+ {
605+ HonkRecursionConstraintsOutput<Builder> output;
538606
539- output.finalize (builder, is_hn_recursion_constraints, metadata.has_ipa_claim );
607+ // Merge outputs from honk recursion tasks that ran in Phase 3
608+ for (size_t i = 0 ; i < rec_out_idx; i++) {
609+ const auto & rec = recursion_task_outputs[i];
610+ output.update (rec.output , rec.update_ipa_data );
611+ if (rec.is_root_rollup ) {
612+ output.is_root_rollup = true ;
613+ }
614+ }
615+
616+ // Chonk and AVM recursion constraints — Ultra only, sequential for now (TODO: parallelize)
617+ if constexpr (std::is_same_v<Builder, UltraCircuitBuilder>) {
618+ for (const auto & constraint : constraints.chonk_recursion_constraints ) {
619+ auto honk_output = create_chonk_recursion_constraints (builder, constraint);
620+ output.update (honk_output, /* update_ipa_data=*/ true );
621+ }
622+ for (const auto & constraint : constraints.avm_recursion_constraints ) {
623+ auto honk_output = create_avm2_recursion_constraints_goblin (builder, constraint);
624+ output.update (honk_output, /* update_ipa_data=*/ true );
625+ }
626+ }
627+
628+ // HyperNova recursion constraints (Mega only, requires IVC state — always sequential)
629+ const bool is_hn_recursion_constraints = !constraints.hn_recursion_constraints .empty ();
630+ if (is_hn_recursion_constraints) {
631+ GateCounter gate_counter{ &builder, false };
632+ std::vector<size_t > dummy_gates_per_opcode;
633+ auto hn_output = create_recursion_constraints<Builder>(
634+ builder,
635+ gate_counter,
636+ dummy_gates_per_opcode,
637+ metadata.ivc ,
638+ { {}, {} },
639+ { {}, {} },
640+ { constraints.hn_recursion_constraints , constraints.original_opcode_indices .hn_recursion_constraints },
641+ { {}, {} });
642+ output.update (hn_output, /* update_ipa_data=*/ false );
643+ }
644+
645+ output.finalize (builder, is_hn_recursion_constraints, metadata.has_ipa_claim );
646+ }
540647}
541648
542649template void build_constraints_parallel<UltraCircuitBuilder>(UltraCircuitBuilder&,
0 commit comments