Skip to main content

xlog_runtime/executor/
epistemic_workspace.rs

1//! Epistemic GPU workspace allocation.
2
3use std::{collections::BTreeSet, ffi::c_void, sync::Arc};
4
5use cudarc::driver::LaunchConfig;
6use xlog_core::{RelId, Result, ScalarType, Schema, XlogError};
7use xlog_cuda::provider::{
8    epistemic_kernels, HostLaunchMetadataTransferStats, HostTransferStats, EPISTEMIC_MODULE,
9};
10use xlog_cuda::{
11    memory::{validate_logical_row_count, TrackedCudaSlice},
12    sys, AsKernelParam, CudaBuffer, CudaColumn, DeviceSlice, DriverError, LaunchAsync,
13};
14use xlog_ir::rir::{MultiwayPlan, PlannedHashReason, RirNode, StreamGroupId};
15use xlog_ir::{
16    EirEpistemicMode, EirEpistemicOp, EirTerm, EpistemicExecutablePlan, EpistemicExecutionBackend,
17    EpistemicFallbackPolicy, EpistemicGpuBufferKind, EpistemicGpuHotPathPhase, EpistemicGpuPlan,
18    EpistemicTupleMembershipBinding, EpistemicWcojReductionStatus,
19};
20
21use super::Executor;
22
23const XLOG_CONSTRAINT_RELATION_PREFIX: &str = "__xlog_constraint_";
24
25/// Capacity limits for an epistemic GPU workspace allocation.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct EpistemicGpuWorkspaceCapacities {
28    /// Maximum generated epistemic candidates.
29    pub max_candidates: usize,
30    /// Maximum worlds tracked per candidate.
31    pub max_worlds: usize,
32    /// Maximum reduced-program models tracked per reduction.
33    pub max_models_per_reduction: usize,
34}
35
36/// Concrete device-buffer layout for an epistemic GPU workspace.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct EpistemicGpuWorkspaceLayout {
39    /// Candidate-assumption buffer size in bytes.
40    pub candidate_assumption_bytes: usize,
41    /// World-view buffer size in bytes.
42    pub world_view_bytes: usize,
43    /// Model-membership buffer size in bytes.
44    pub model_membership_bytes: usize,
45    /// Rejection-reason slot count.
46    pub rejection_reason_slots: usize,
47}
48
49impl EpistemicGpuWorkspaceLayout {
50    /// Build a workspace layout from an epistemic GPU plan and capacity limits.
51    pub fn for_plan(
52        plan: &EpistemicGpuPlan,
53        capacities: EpistemicGpuWorkspaceCapacities,
54    ) -> Result<Self> {
55        require_positive(
56            capacities.max_candidates,
57            "epistemic GPU workspace candidates",
58        )?;
59        require_positive(capacities.max_worlds, "epistemic GPU workspace worlds")?;
60        require_positive(
61            capacities.max_models_per_reduction,
62            "epistemic GPU workspace models",
63        )?;
64        require_positive(
65            plan.epistemic_literals.len(),
66            "epistemic GPU workspace literals",
67        )?;
68        require_positive(plan.reductions.len(), "epistemic GPU workspace reductions")?;
69
70        let literal_count = plan.epistemic_literals.len();
71        let reduction_count = plan.reductions.len();
72        let candidate_assumption_bytes = checked_product(capacities.max_candidates, literal_count)?;
73        let world_view_stride = capacities
74            .max_worlds
75            .max(world_view_bitset_bytes_per_candidate(literal_count)?);
76        let world_view_bytes = checked_product(capacities.max_candidates, world_view_stride)?;
77        let model_membership_bytes = checked_product(
78            checked_product(
79                checked_product(
80                    capacities.max_candidates,
81                    capacities.max_models_per_reduction,
82                )?,
83                reduction_count,
84            )?,
85            literal_count,
86        )?;
87
88        Ok(Self {
89            candidate_assumption_bytes,
90            world_view_bytes,
91            model_membership_bytes,
92            rejection_reason_slots: capacities.max_candidates,
93        })
94    }
95
96    /// Total workspace byte size across every device buffer category.
97    pub fn total_bytes(&self) -> usize {
98        self.try_total_bytes()
99            .expect("epistemic GPU workspace layout byte total overflowed")
100    }
101
102    /// Checked total workspace byte size across every device buffer category.
103    pub fn try_total_bytes(&self) -> Result<usize> {
104        let rejection_reason_bytes =
105            checked_product(self.rejection_reason_slots, std::mem::size_of::<u32>())?;
106        checked_sum(
107            checked_sum(
108                checked_sum(self.candidate_assumption_bytes, self.world_view_bytes)?,
109                self.model_membership_bytes,
110            )?,
111            rejection_reason_bytes,
112        )
113    }
114}
115
116/// Device-resident buffers for epistemic Generate-Propagate-Test execution.
117pub struct EpistemicGpuWorkspace {
118    /// Workspace layout used for allocation.
119    pub layout: EpistemicGpuWorkspaceLayout,
120    /// Candidate-assumption bitset buffer.
121    pub candidate_assumptions: TrackedCudaSlice<u8>,
122    /// Candidate and accepted world-view bitset buffer.
123    pub world_views: TrackedCudaSlice<u8>,
124    /// Per-model membership check buffer.
125    pub model_membership: TrackedCudaSlice<u8>,
126    /// Structured rejection-reason code buffer.
127    pub rejection_reasons: TrackedCudaSlice<u32>,
128    /// Per-candidate firing integrity-constraint index buffer. Parallel to
129    /// `rejection_reasons`, sized `layout.rejection_reason_slots`. Holds the
130    /// declaration-order index of the constraint that rejected a candidate, or
131    /// the sentinel `u32::MAX` when no integrity constraint rejected it. The
132    /// reason code in `rejection_reasons` is left at 6 for constraint
133    /// violations; this buffer adds the constraint-specific detail.
134    pub constraint_violation_index: TrackedCudaSlice<u32>,
135}
136
137impl EpistemicGpuWorkspace {
138    /// Require retained device buffers to match the certified workspace layout.
139    pub fn require_buffer_lengths_match_layout(&self, construct: &str) -> Result<()> {
140        if self.candidate_assumptions.len() != self.layout.candidate_assumption_bytes
141            || self.world_views.len() != self.layout.world_view_bytes
142            || self.model_membership.len() != self.layout.model_membership_bytes
143            || self.rejection_reasons.len() != self.layout.rejection_reason_slots
144            || self.constraint_violation_index.len() != self.layout.rejection_reason_slots
145        {
146            return Err(XlogError::UnsupportedEpistemicConstruct {
147                construct: construct.to_string(),
148                context: format!(
149                    "prepared GPU workspace buffer lengths do not match layout: \
150                     candidate_bytes={}/{} world_view_bytes={}/{} model_membership_bytes={}/{} \
151                     rejection_reason_slots={}/{} constraint_violation_index_slots={}/{}",
152                    self.candidate_assumptions.len(),
153                    self.layout.candidate_assumption_bytes,
154                    self.world_views.len(),
155                    self.layout.world_view_bytes,
156                    self.model_membership.len(),
157                    self.layout.model_membership_bytes,
158                    self.rejection_reasons.len(),
159                    self.layout.rejection_reason_slots,
160                    self.constraint_violation_index.len(),
161                    self.layout.rejection_reason_slots
162                ),
163            });
164        }
165
166        Ok(())
167    }
168}
169
170/// Trace proving an epistemic GPU workspace was initialized on device.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct EpistemicGpuWorkspaceResetTrace {
173    /// Candidate-assumption bytes zeroed on device.
174    pub candidate_assumption_bytes: usize,
175    /// World-view bytes zeroed on device.
176    pub world_view_bytes: usize,
177    /// Model-membership bytes zeroed on device.
178    pub model_membership_bytes: usize,
179    /// Rejection-reason bytes zeroed on device.
180    pub rejection_reason_bytes: usize,
181    /// Device zeroing operations submitted by the reset path.
182    pub device_zero_ops: u32,
183    /// Host writes used by the reset path. Accepted GPU execution requires zero.
184    pub host_write_ops: u32,
185}
186
187/// CUDA-event timing captured around one epistemic GPU kernel launch.
188#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
189pub struct EpistemicGpuKernelTimingTrace {
190    /// CUDA event pairs recorded around the launch. Runtime traces require one.
191    pub cuda_event_pairs: u32,
192    /// CUDA event synchronizations used to make elapsed time observable on host.
193    pub timing_sync_ops: u32,
194    /// Event-measured stream elapsed time, converted from milliseconds to nanoseconds.
195    pub kernel_elapsed_nanos: u64,
196}
197
198/// Trace proving candidate assumptions were generated by a GPU kernel.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub struct EpistemicGpuCandidateGenerationTrace {
201    /// Number of epistemic literals represented per candidate.
202    pub literal_count: usize,
203    /// Number of candidate rows generated on device.
204    pub generated_candidates: usize,
205    /// Candidate-assumption bytes written by the kernel.
206    pub candidate_assumption_bytes: usize,
207    /// Candidate-generation kernel launches.
208    pub kernel_launches: u32,
209    /// Host writes used by candidate generation. Accepted execution requires zero.
210    pub host_write_ops: u32,
211    /// CUDA-event timing for the launched kernel.
212    pub kernel_timing: EpistemicGpuKernelTimingTrace,
213}
214
215/// Trace proving staged candidate buffers were validated by a GPU kernel.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct EpistemicGpuCandidateValidationTrace {
218    /// Number of epistemic literals represented per candidate.
219    pub literal_count: usize,
220    /// Number of candidate rows validated on device.
221    pub validated_candidates: usize,
222    /// Candidate-assumption bytes checked by the kernel.
223    pub candidate_assumption_bytes_checked: usize,
224    /// World-view staging bytes checked by the kernel.
225    pub world_view_bytes_checked: usize,
226    /// Rejection-reason slots written by the kernel.
227    pub rejection_reason_slots_written: usize,
228    /// Candidate-validation kernel launches.
229    pub kernel_launches: u32,
230    /// Host writes used by validation. Accepted GPU execution requires zero.
231    pub host_write_ops: u32,
232    /// CUDA-event timing for the launched kernel.
233    pub kernel_timing: EpistemicGpuKernelTimingTrace,
234}
235
236/// Trace proving accepted-candidate materialization staging used a GPU kernel.
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub struct EpistemicGpuMaterializationTrace {
239    /// Number of candidate rows materialized on device.
240    pub materialized_candidates: usize,
241    /// World-view slots written by the kernel.
242    pub world_view_slots_written: usize,
243    /// Materialization kernel launches.
244    pub kernel_launches: u32,
245    /// Host writes used by materialization. Accepted GPU execution requires zero.
246    pub host_write_ops: u32,
247    /// CUDA-event timing for the launched kernel.
248    pub kernel_timing: EpistemicGpuKernelTimingTrace,
249}
250
251/// Trace proving final result flags were materialized from device-side output metadata.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct EpistemicGpuFinalResultMaterializationTrace {
254    /// Number of candidate rows materialized on device.
255    pub materialized_candidates: usize,
256    /// Device output row-count scalars read by the kernel.
257    pub output_row_count_device_reads: u32,
258    /// World-view result slots written by the kernel.
259    pub world_view_slots_written: usize,
260    /// Final-result materialization kernel launches.
261    pub kernel_launches: u32,
262    /// Host writes used by final-result materialization. Accepted execution requires zero.
263    pub host_write_ops: u32,
264    /// CUDA-event timing for the launched kernel.
265    pub kernel_timing: EpistemicGpuKernelTimingTrace,
266}
267
268/// Trace proving final query tuples were materialized into a device-resident buffer.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct EpistemicGpuFinalTupleMaterializationTrace {
271    /// Number of output columns copied into the final device buffer.
272    pub output_column_count: usize,
273    /// Row capacity of the final output buffer.
274    pub output_row_capacity: usize,
275    /// Device tuple bytes covered by the materialization kernels.
276    pub tuple_bytes_capacity: usize,
277    /// Device output row-count scalars read by the kernels.
278    pub output_row_count_device_reads: u32,
279    /// Model-membership bytes checked by the kernels before tuple materialization.
280    pub model_membership_bytes_checked: usize,
281    /// Bounded model slots available per reduction during final tuple materialization.
282    pub bounded_model_slots_per_reduction: usize,
283    /// Output row capacity that can be checked against row-specific model slots.
284    pub row_specific_membership_row_capacity: usize,
285    /// Output row capacity beyond the bounded model-slot window.
286    pub row_filter_row_capacity_outside_model_slot_window: usize,
287    /// World-view slots checked by the kernels before tuple materialization.
288    pub world_view_slots_checked: usize,
289    /// Variable-bound tuple row filters applied by the final-row map kernel.
290    pub row_filter_count: usize,
291    /// Negated variable-bound tuple row filters applied by the final-row map kernel.
292    pub negated_row_filter_count: usize,
293    /// Device final row-count scalars written by the kernels.
294    pub final_row_count_device_writes: u32,
295    /// Final tuple materialization kernel launches.
296    pub kernel_launches: u32,
297    /// Host writes used by final tuple materialization. Accepted execution requires zero.
298    pub host_write_ops: u32,
299    /// CUDA-event timing for the launched kernel batch.
300    pub kernel_timing: EpistemicGpuKernelTimingTrace,
301}
302
303/// Trace proving the epistemic GPU hot path avoided tracked data-plane host transfers.
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub struct EpistemicGpuTransferBudgetTrace {
306    /// Number of candidate rows covered by this transfer-budget check.
307    pub candidate_count: usize,
308    /// Tracked device-to-host bytes observed inside the GPU hot path.
309    pub tracked_dtoh_bytes: u64,
310    /// Tracked data-plane host-to-device bytes observed inside the GPU hot path.
311    pub tracked_htod_bytes: u64,
312    /// Tracked device-to-host calls observed inside the GPU hot path.
313    pub tracked_dtoh_calls: u64,
314    /// Tracked data-plane host-to-device calls observed inside the GPU hot path.
315    pub tracked_htod_calls: u64,
316    /// Tracked aggregate host-to-device bytes observed inside the GPU hot path.
317    pub tracked_aggregate_htod_bytes: u64,
318    /// Tracked aggregate host-to-device calls observed inside the GPU hot path.
319    pub tracked_aggregate_htod_calls: u64,
320    /// Tracked launch-metadata host-to-device bytes observed inside the GPU hot path.
321    pub tracked_launch_metadata_htod_bytes: u64,
322    /// Tracked launch-metadata host-to-device calls observed inside the GPU hot path.
323    pub tracked_launch_metadata_htod_calls: u64,
324    /// Tracked data-plane host-to-device bytes observed inside the GPU hot path.
325    pub tracked_data_plane_htod_bytes: u64,
326    /// Tracked data-plane host-to-device calls observed inside the GPU hot path.
327    pub tracked_data_plane_htod_calls: u64,
328    /// Per-candidate host round trips observed inside the GPU hot path.
329    pub per_candidate_host_round_trips: u64,
330}
331
332/// Trace accounting for the bounded final-result transfer after the GPU hot path.
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub struct EpistemicGpuFinalResultTransferTrace {
335    /// Logical rows in the final device-resident output buffer.
336    pub final_output_rows: usize,
337    /// Number of final output columns that a caller may export.
338    pub final_output_column_count: usize,
339    /// Bytes in one final output row.
340    pub final_output_row_width_bytes: usize,
341    /// Bounded data-plane payload bytes represented by the final output.
342    pub final_output_payload_bytes: u64,
343    /// Device row-count metadata reads used for this accounting.
344    pub row_count_device_reads: u32,
345    /// Data-plane device-to-host calls issued by accepted execution. Execution returns a device buffer, so this is zero.
346    pub tracked_data_plane_dtoh_calls: u64,
347    /// Data-plane device-to-host bytes issued by accepted execution. Execution returns a device buffer, so this is zero.
348    pub tracked_data_plane_dtoh_bytes: u64,
349}
350
351/// Bounded validation of reduced integrity-constraint relations after GPU execution.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct EpistemicGpuConstraintValidationTrace {
354    /// Number of compiler-generated `__xlog_constraint_N` relations checked.
355    pub checked_constraint_relations: usize,
356    /// Number of checked constraint relations that contained violating rows.
357    pub violated_constraint_relations: usize,
358    /// Constraint row-count reads that had to consult device metadata.
359    pub row_count_device_reads: u32,
360}
361
362impl EpistemicGpuConstraintValidationTrace {
363    /// Require reduced integrity-constraint validation to match preflight obligations.
364    pub fn require_matches_preflight(
365        &self,
366        construct: &str,
367        preflight: &EpistemicGpuRuntimePreflight,
368    ) -> Result<()> {
369        if self.checked_constraint_relations != preflight.reduced_constraint_relation_count
370            || self.violated_constraint_relations != 0
371            || self.row_count_device_reads as usize > self.checked_constraint_relations
372        {
373            return Err(XlogError::UnsupportedEpistemicConstruct {
374                construct: construct.to_string(),
375                context: format!(
376                    "constraint validation trace must match reduced runtime preflight, got \
377                     checked={} expected_checked={} violations={} row_count_reads={}",
378                    self.checked_constraint_relations,
379                    preflight.reduced_constraint_relation_count,
380                    self.violated_constraint_relations,
381                    self.row_count_device_reads
382                ),
383            });
384        }
385
386        Ok(())
387    }
388}
389
390/// Typed interpretation of nonzero GPU epistemic rejection codes.
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub enum EpistemicGpuRejectionReason {
393    /// Candidate was rejected because its world-view row was inactive.
394    InactiveWorld,
395    /// Candidate buffer contained a value outside the valid boolean bit range.
396    InvalidCandidateBit,
397    /// Candidate did not have a reduced-model tuple source to validate against.
398    MissingReducedModel,
399    /// Candidate assumptions were not supported by model-membership evidence.
400    UnsatisfiedMembership,
401    /// Accepted world view satisfied an epistemic integrity constraint body.
402    WorldViewConstraintViolation,
403}
404
405impl EpistemicGpuRejectionReason {
406    /// Return the raw device rejection code used by the CUDA kernels.
407    pub const fn code(self) -> u32 {
408        match self {
409            Self::InactiveWorld => 2,
410            Self::InvalidCandidateBit => 3,
411            Self::MissingReducedModel => 4,
412            Self::UnsatisfiedMembership => 5,
413            Self::WorldViewConstraintViolation => 6,
414        }
415    }
416
417    /// Decode a nonzero device rejection code into a typed reason.
418    pub fn from_code(code: u32) -> Result<Self> {
419        match code {
420            2 => Ok(Self::InactiveWorld),
421            3 => Ok(Self::InvalidCandidateBit),
422            4 => Ok(Self::MissingReducedModel),
423            5 => Ok(Self::UnsatisfiedMembership),
424            6 => Ok(Self::WorldViewConstraintViolation),
425            other => Err(XlogError::UnsupportedEpistemicConstruct {
426                construct: "epistemic GPU rejection reason".to_string(),
427                context: format!("unknown device rejection code {other}"),
428            }),
429        }
430    }
431}
432
433/// Device-derived semantic summary for Generate-Propagate-Test execution.
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct EpistemicGpuSemanticTrace {
436    /// Number of candidate rows generated on device.
437    pub generated_candidates: usize,
438    /// Number of epistemic guesses represented by generated candidate rows.
439    pub guesses: usize,
440    /// Number of candidate rows propagated on device.
441    pub propagated_candidates: usize,
442    /// Number of generated candidates not propagated.
443    pub pruned_candidates: usize,
444    /// Number of candidate rows checked by world-view validation.
445    pub tested_candidates: usize,
446    /// Number of reduced model slots checked by model-membership/world-view kernels.
447    pub reduced_model_slots_checked: usize,
448    /// Number of accepted candidates observed in the device rejection buffer.
449    pub accepted_candidates: usize,
450    /// Candidate indices accepted by the device rejection buffer.
451    pub accepted_candidate_indices: Vec<usize>,
452    /// Number of accepted world views represented by accepted candidates.
453    pub accepted_world_views: usize,
454    /// Number of rejected candidates observed in the device rejection buffer.
455    pub rejected_candidates: usize,
456    /// Candidate indices rejected by the device rejection buffer.
457    pub rejected_candidate_indices: Vec<usize>,
458    /// Nonzero rejection reason codes copied from the device rejection buffer.
459    pub rejection_reasons: Vec<u32>,
460    /// Constraint-specific reason per rejected candidate, aligned 1:1 with
461    /// `rejected_candidate_indices`. `Some(idx)` when an integrity constraint
462    /// (reason code 6) rejected the candidate, where `idx` is the firing
463    /// constraint's declaration-order index; `None` for every other rejection
464    /// reason. Surfaces constraint-specific rejection detail.
465    pub constraint_violation_indices: Vec<Option<u32>>,
466    /// Bytes observed in the bounded rejection-reason metadata read after the hot path.
467    pub rejection_reason_metadata_bytes: u64,
468}
469
470/// Trace proving model-membership staging was performed by a GPU kernel.
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub struct EpistemicGpuModelMembershipTrace {
473    /// Number of epistemic literals represented per candidate/model.
474    pub literal_count: usize,
475    /// Number of candidate rows checked on device.
476    pub candidates_checked: usize,
477    /// Number of reduced-program summaries represented in the membership layout.
478    pub reduction_count: usize,
479    /// Maximum models represented per reduction.
480    pub models_per_reduction: usize,
481    /// Model-membership bytes written by the kernel.
482    pub model_membership_bytes_written: usize,
483    /// Device output row-count scalars read by the kernel.
484    pub output_row_count_device_reads: u32,
485    /// Device tuple-source row-count scalars read by the kernel.
486    pub tuple_source_row_count_device_reads: u32,
487    /// Device tuple-key columns read by tuple-source membership kernels.
488    pub tuple_source_key_column_device_reads: u32,
489    /// Rejection-reason slots checked by the kernel.
490    pub rejection_reason_slots_checked: usize,
491    /// Source used to populate model-membership bytes.
492    pub membership_source: EpistemicGpuModelMembershipSource,
493    /// Model-membership staging kernel launches.
494    pub kernel_launches: u32,
495    /// Host writes used by model-membership staging. Accepted execution requires zero.
496    pub host_write_ops: u32,
497    /// CUDA-event timing for the launched kernel.
498    pub kernel_timing: EpistemicGpuKernelTimingTrace,
499}
500
501/// Source of GPU model-membership bytes for epistemic world-view validation.
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum EpistemicGpuModelMembershipSource {
504    /// Current bounded staging only proves the reduced output has rows.
505    ReducedOutputRowCountOnly,
506    /// Model-membership bytes were populated from reduced stable-model tuple buffers.
507    StableModelTupleBuffer,
508}
509
510/// Trace proving staged model memberships were validated against world views on GPU.
511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512pub struct EpistemicGpuWorldViewValidationTrace {
513    /// Number of epistemic literals represented per candidate/model.
514    pub literal_count: usize,
515    /// Number of candidate rows checked on device.
516    pub candidates_checked: usize,
517    /// Number of reduced-program summaries represented in the membership layout.
518    pub reduction_count: usize,
519    /// Maximum models represented per reduction.
520    pub models_per_reduction: usize,
521    /// Model-membership bytes checked by the kernel.
522    pub model_membership_bytes_checked: usize,
523    /// World-view staging slots checked by the kernel.
524    pub world_view_slots_checked: usize,
525    /// Rejection-reason slots written by the kernel.
526    pub rejection_reason_slots_written: usize,
527    /// World-view validation kernel launches.
528    pub kernel_launches: u32,
529    /// Host writes used by world-view validation. Accepted execution requires zero.
530    pub host_write_ops: u32,
531    /// CUDA-event timing for the launched kernel.
532    pub kernel_timing: EpistemicGpuKernelTimingTrace,
533}
534
535/// Trace proving epistemic integrity constraints were evaluated against world
536/// views on GPU.
537///
538/// World-view integrity constraints (`:- know unsafe().`) prune accepted
539/// candidate world views on device after modal world-view validation. The
540/// device kernel never reads accepted worlds back to the host, so accepted
541/// execution keeps `host_write_ops` at zero.
542#[derive(Debug, Clone, Copy, PartialEq, Eq)]
543pub struct EpistemicGpuConstraintWorldViewValidationTrace {
544    /// Number of epistemic integrity constraints checked on device.
545    pub constraint_count: usize,
546    /// Number of constraint-body literal references checked on device.
547    pub constraint_literal_refs: usize,
548    /// Number of candidate world views checked by the constraint kernel.
549    pub candidates_checked: usize,
550    /// Rejection-reason slots written by the kernel.
551    pub rejection_reason_slots_written: usize,
552    /// Constraint world-view validation kernel launches.
553    pub kernel_launches: u32,
554    /// Host writes used by constraint validation. Accepted execution requires zero.
555    pub host_write_ops: u32,
556    /// CUDA-event timing for the launched kernel.
557    pub kernel_timing: EpistemicGpuKernelTimingTrace,
558}
559
560/// Trace proving candidate propagation staging was performed by a GPU kernel.
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub struct EpistemicGpuPropagationTrace {
563    /// Number of epistemic literals represented per candidate.
564    pub literal_count: usize,
565    /// Number of candidate rows propagated on device.
566    pub propagated_candidates: usize,
567    /// World-view staging bytes written by the kernel.
568    pub world_view_bytes_written: usize,
569    /// Rejection-reason slots initialized by the kernel.
570    pub rejection_reason_slots_written: usize,
571    /// Candidate-propagation kernel launches.
572    pub kernel_launches: u32,
573    /// Host writes used by propagation. Accepted GPU execution requires zero.
574    pub host_write_ops: u32,
575    /// CUDA-event timing for the launched kernel.
576    pub kernel_timing: EpistemicGpuKernelTimingTrace,
577}
578
579impl EpistemicGpuKernelTimingTrace {
580    /// Empty timing marker used before a runtime launch records CUDA events.
581    pub const fn unrecorded() -> Self {
582        Self {
583            cuda_event_pairs: 0,
584            timing_sync_ops: 0,
585            kernel_elapsed_nanos: 0,
586        }
587    }
588
589    /// Convert CUDA's native event elapsed time in milliseconds to a trace.
590    pub fn from_cuda_elapsed_ms(elapsed_ms: f32) -> Result<Self> {
591        if !elapsed_ms.is_finite() || elapsed_ms < 0.0 {
592            return Err(XlogError::Execution(format!(
593                "invalid epistemic GPU kernel elapsed time: {elapsed_ms}"
594            )));
595        }
596        let elapsed_nanos = ((elapsed_ms as f64) * 1_000_000.0).round();
597        if elapsed_nanos >= u64::MAX as f64 {
598            return Err(XlogError::UnsupportedEpistemicConstruct {
599                construct: "epistemic GPU kernel timing trace".to_string(),
600                context: format!(
601                    "CUDA elapsed time {elapsed_ms}ms exceeds the u64 nanosecond trace counter"
602                ),
603            });
604        }
605
606        Ok(Self {
607            cuda_event_pairs: 1,
608            timing_sync_ops: 1,
609            kernel_elapsed_nanos: elapsed_nanos as u64,
610        })
611    }
612
613    /// Whether CUDA-event timing was recorded for this trace.
614    pub const fn is_recorded(&self) -> bool {
615        self.cuda_event_pairs > 0 && self.timing_sync_ops > 0
616    }
617
618    /// Saturating sum used when aggregating multi-kernel or split-batch traces.
619    pub fn saturating_add(self, other: Self) -> Self {
620        Self {
621            cuda_event_pairs: self.cuda_event_pairs.saturating_add(other.cuda_event_pairs),
622            timing_sync_ops: self.timing_sync_ops.saturating_add(other.timing_sync_ops),
623            kernel_elapsed_nanos: self
624                .kernel_elapsed_nanos
625                .saturating_add(other.kernel_elapsed_nanos),
626        }
627    }
628
629    /// Checked sum used by accepted certification paths.
630    pub fn checked_add(self, other: Self) -> Result<Self> {
631        Ok(Self {
632            cuda_event_pairs: self
633                .cuda_event_pairs
634                .checked_add(other.cuda_event_pairs)
635                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
636                    construct: "epistemic GPU kernel timing trace".to_string(),
637                    context: format!(
638                        "CUDA event-pair counter overflowed while adding {} to {}",
639                        other.cuda_event_pairs, self.cuda_event_pairs
640                    ),
641                })?,
642            timing_sync_ops: self
643                .timing_sync_ops
644                .checked_add(other.timing_sync_ops)
645                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
646                    construct: "epistemic GPU kernel timing trace".to_string(),
647                    context: format!(
648                        "CUDA timing-sync counter overflowed while adding {} to {}",
649                        other.timing_sync_ops, self.timing_sync_ops
650                    ),
651                })?,
652            kernel_elapsed_nanos: self
653                .kernel_elapsed_nanos
654                .checked_add(other.kernel_elapsed_nanos)
655                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
656                    construct: "epistemic GPU kernel timing trace".to_string(),
657                    context: format!(
658                        "kernel elapsed-time counter overflowed while adding {} to {}",
659                        other.kernel_elapsed_nanos, self.kernel_elapsed_nanos
660                    ),
661                })?,
662        })
663    }
664
665    /// Aggregate timing traces from a single execution or split-batch result.
666    pub fn sum(traces: impl IntoIterator<Item = Self>) -> Self {
667        traces
668            .into_iter()
669            .fold(Self::unrecorded(), Self::saturating_add)
670    }
671
672    /// Checked aggregate timing traces for accepted certification paths.
673    pub fn checked_sum(traces: impl IntoIterator<Item = Self>) -> Result<Self> {
674        traces
675            .into_iter()
676            .try_fold(Self::unrecorded(), Self::checked_add)
677    }
678}
679
680impl EpistemicGpuCandidateGenerationTrace {
681    /// Build a candidate-generation trace for a bounded device launch.
682    pub fn for_counts(literal_count: usize, candidate_count: usize) -> Result<Self> {
683        require_positive(literal_count, "epistemic GPU candidate literals")?;
684        require_positive(candidate_count, "epistemic GPU candidate count")?;
685        if literal_count > 31 {
686            return Err(XlogError::UnsupportedEpistemicConstruct {
687                construct: "epistemic GPU candidate generation".to_string(),
688                context: format!("literal count {literal_count} exceeds 31-bit candidate mask"),
689            });
690        }
691        if candidate_count > (1usize << literal_count) {
692            return Err(XlogError::ResourceExhausted {
693                context: "epistemic GPU candidate count".to_string(),
694                estimated_bytes: candidate_count as u64,
695                budget_bytes: (1usize << literal_count) as u64,
696            });
697        }
698
699        let candidate_assumption_bytes = checked_product(literal_count, candidate_count)?;
700        require_u32_launch_bound(
701            candidate_assumption_bytes,
702            "epistemic GPU candidate generation launch",
703        )?;
704
705        Ok(Self {
706            literal_count,
707            generated_candidates: candidate_count,
708            candidate_assumption_bytes,
709            kernel_launches: 1,
710            host_write_ops: 0,
711            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
712        })
713    }
714
715    /// Attach CUDA-event timing captured by the runtime launch path.
716    pub const fn with_kernel_timing(
717        mut self,
718        kernel_timing: EpistemicGpuKernelTimingTrace,
719    ) -> Self {
720        self.kernel_timing = kernel_timing;
721        self
722    }
723}
724
725impl EpistemicGpuCandidateValidationTrace {
726    /// Build a validation trace for a bounded device launch.
727    pub fn for_counts(literal_count: usize, candidate_count: usize) -> Result<Self> {
728        require_positive(literal_count, "epistemic GPU candidate validation literals")?;
729        require_positive(
730            candidate_count,
731            "epistemic GPU candidate validation candidates",
732        )?;
733        require_u32_launch_dimensions(
734            &[literal_count, candidate_count],
735            "epistemic GPU validation launch",
736        )?;
737        let candidate_assumption_bytes_checked = checked_product(literal_count, candidate_count)?;
738
739        Ok(Self {
740            literal_count,
741            validated_candidates: candidate_count,
742            candidate_assumption_bytes_checked,
743            world_view_bytes_checked: candidate_count,
744            rejection_reason_slots_written: candidate_count,
745            kernel_launches: 1,
746            host_write_ops: 0,
747            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
748        })
749    }
750
751    /// Attach CUDA-event timing captured by the runtime launch path.
752    pub const fn with_kernel_timing(
753        mut self,
754        kernel_timing: EpistemicGpuKernelTimingTrace,
755    ) -> Self {
756        self.kernel_timing = kernel_timing;
757        self
758    }
759
760    /// Require validation coverage to match the generated candidate workspace.
761    pub fn require_matches_candidate_generation(
762        &self,
763        construct: &str,
764        candidate_generation: &EpistemicGpuCandidateGenerationTrace,
765    ) -> Result<()> {
766        let expected_world_view_bytes = checked_product(
767            world_view_bitset_bytes_per_candidate(candidate_generation.literal_count)?,
768            candidate_generation.generated_candidates,
769        )?;
770        if self.literal_count != candidate_generation.literal_count
771            || self.validated_candidates != candidate_generation.generated_candidates
772            || self.candidate_assumption_bytes_checked
773                != candidate_generation.candidate_assumption_bytes
774            || self.world_view_bytes_checked != expected_world_view_bytes
775            || self.rejection_reason_slots_written != candidate_generation.generated_candidates
776        {
777            return Err(XlogError::UnsupportedEpistemicConstruct {
778                construct: construct.to_string(),
779                context: format!(
780                    "candidate validation trace does not match generated GPU candidates: \
781                     literals={}/{} candidates={}/{} candidate_bytes={}/{} \
782                     world_view_bytes={}/{} rejection_slots={}/{}",
783                    self.literal_count,
784                    candidate_generation.literal_count,
785                    self.validated_candidates,
786                    candidate_generation.generated_candidates,
787                    self.candidate_assumption_bytes_checked,
788                    candidate_generation.candidate_assumption_bytes,
789                    self.world_view_bytes_checked,
790                    expected_world_view_bytes,
791                    self.rejection_reason_slots_written,
792                    candidate_generation.generated_candidates
793                ),
794            });
795        }
796
797        Ok(())
798    }
799}
800
801impl EpistemicGpuMaterializationTrace {
802    /// Build a materialization trace for a bounded device launch.
803    pub fn for_count(candidate_count: usize) -> Result<Self> {
804        require_positive(candidate_count, "epistemic GPU materialization candidates")?;
805        require_u32_launch_bound(candidate_count, "epistemic GPU materialization launch")?;
806
807        Ok(Self {
808            materialized_candidates: candidate_count,
809            world_view_slots_written: candidate_count,
810            kernel_launches: 1,
811            host_write_ops: 0,
812            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
813        })
814    }
815
816    /// Attach CUDA-event timing captured by the runtime launch path.
817    pub const fn with_kernel_timing(
818        mut self,
819        kernel_timing: EpistemicGpuKernelTimingTrace,
820    ) -> Self {
821        self.kernel_timing = kernel_timing;
822        self
823    }
824}
825
826impl EpistemicGpuFinalResultMaterializationTrace {
827    /// Build a final-result materialization trace for a bounded device launch.
828    pub fn for_count(candidate_count: usize) -> Result<Self> {
829        require_positive(
830            candidate_count,
831            "epistemic GPU final-result materialization candidates",
832        )?;
833        require_u32_launch_bound(candidate_count, "epistemic GPU final-result launch")?;
834
835        Ok(Self {
836            materialized_candidates: candidate_count,
837            output_row_count_device_reads: 1,
838            world_view_slots_written: candidate_count,
839            kernel_launches: 1,
840            host_write_ops: 0,
841            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
842        })
843    }
844
845    /// Attach CUDA-event timing captured by the runtime launch path.
846    pub const fn with_kernel_timing(
847        mut self,
848        kernel_timing: EpistemicGpuKernelTimingTrace,
849    ) -> Self {
850        self.kernel_timing = kernel_timing;
851        self
852    }
853}
854
855impl EpistemicGpuFinalTupleMaterializationTrace {
856    /// Build a final tuple materialization trace for a device-side output buffer.
857    pub fn for_counts(
858        output_column_count: usize,
859        output_row_capacity: usize,
860        tuple_bytes_capacity: usize,
861        literal_count: usize,
862        candidate_count: usize,
863        reduction_count: usize,
864        models_per_reduction: usize,
865    ) -> Result<Self> {
866        if output_column_count > u32::MAX as usize {
867            return Err(XlogError::ResourceExhausted {
868                context: "epistemic GPU final-tuple output columns".to_string(),
869                estimated_bytes: output_column_count as u64,
870                budget_bytes: u32::MAX as u64,
871            });
872        }
873        require_u32_launch_bound(output_row_capacity, "epistemic GPU final-tuple output rows")?;
874        require_positive(literal_count, "epistemic GPU final-tuple literals")?;
875        require_positive(candidate_count, "epistemic GPU final-tuple candidates")?;
876        require_positive(reduction_count, "epistemic GPU final-tuple reductions")?;
877        require_positive(models_per_reduction, "epistemic GPU final-tuple models")?;
878        let model_membership_bytes_checked = checked_product(
879            checked_product(
880                checked_product(candidate_count, reduction_count)?,
881                models_per_reduction,
882            )?,
883            literal_count,
884        )?;
885        require_u32_launch_bound(
886            model_membership_bytes_checked,
887            "epistemic GPU final-tuple membership launch",
888        )?;
889        let output_row_count_device_reads = checked_sum(output_column_count, 1)?;
890        let kernel_launches = checked_sum(output_row_count_device_reads, 1)?;
891        if kernel_launches > u32::MAX as usize {
892            return Err(XlogError::ResourceExhausted {
893                context: "epistemic GPU final-tuple kernel launches".to_string(),
894                estimated_bytes: kernel_launches as u64,
895                budget_bytes: u32::MAX as u64,
896            });
897        }
898
899        Ok(Self {
900            output_column_count,
901            output_row_capacity,
902            tuple_bytes_capacity,
903            output_row_count_device_reads: output_row_count_device_reads as u32,
904            model_membership_bytes_checked,
905            bounded_model_slots_per_reduction: models_per_reduction,
906            row_specific_membership_row_capacity: 0,
907            row_filter_row_capacity_outside_model_slot_window: 0,
908            world_view_slots_checked: candidate_count,
909            row_filter_count: 0,
910            negated_row_filter_count: 0,
911            final_row_count_device_writes: 1,
912            kernel_launches: kernel_launches as u32,
913            host_write_ops: 0,
914            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
915        })
916    }
917
918    /// Attach CUDA-event timing captured by the runtime launch path.
919    pub const fn with_kernel_timing(
920        mut self,
921        kernel_timing: EpistemicGpuKernelTimingTrace,
922    ) -> Self {
923        self.kernel_timing = kernel_timing;
924        self
925    }
926
927    /// Attach final-row filter metadata captured before launching the row-map kernel.
928    pub fn with_row_filter_counts(
929        mut self,
930        row_filter_count: usize,
931        negated_row_filter_count: usize,
932    ) -> Result<Self> {
933        if negated_row_filter_count > row_filter_count {
934            return Err(XlogError::ResourceExhausted {
935                context: "epistemic GPU final-tuple negated row filters".to_string(),
936                estimated_bytes: negated_row_filter_count as u64,
937                budget_bytes: row_filter_count as u64,
938            });
939        }
940        self.row_filter_count = row_filter_count;
941        self.negated_row_filter_count = negated_row_filter_count;
942        if row_filter_count > 0 {
943            self.row_specific_membership_row_capacity = self
944                .output_row_capacity
945                .min(self.bounded_model_slots_per_reduction);
946            self.row_filter_row_capacity_outside_model_slot_window = self
947                .output_row_capacity
948                .saturating_sub(self.row_specific_membership_row_capacity);
949        }
950        Ok(self)
951    }
952
953    /// Require GPU evidence that row-filtered tuple output fits the validated coverage window.
954    pub fn require_row_filter_materialization_evidence(
955        &self,
956        construct: &str,
957        final_output_rows: usize,
958    ) -> Result<()> {
959        if final_output_rows > self.output_row_capacity {
960            return Err(XlogError::UnsupportedEpistemicConstruct {
961                construct: construct.to_string(),
962                context: format!(
963                    "final tuple materialization reported {} logical rows for output row \
964                     capacity {}",
965                    final_output_rows, self.output_row_capacity
966                ),
967            });
968        }
969        if self.negated_row_filter_count > self.row_filter_count {
970            return Err(XlogError::UnsupportedEpistemicConstruct {
971                construct: construct.to_string(),
972                context: format!(
973                    "row-filtered final tuple materialization reported {} negated row filters \
974                     for {} total row filters",
975                    self.negated_row_filter_count, self.row_filter_count
976                ),
977            });
978        }
979        if self.row_filter_count == 0 {
980            if self.row_specific_membership_row_capacity != 0
981                || self.row_filter_row_capacity_outside_model_slot_window != 0
982            {
983                return Err(XlogError::UnsupportedEpistemicConstruct {
984                    construct: construct.to_string(),
985                    context: format!(
986                        "final tuple materialization without row filters reported row-filter \
987                         coverage row_specific_capacity={} fallback_capacity={}",
988                        self.row_specific_membership_row_capacity,
989                        self.row_filter_row_capacity_outside_model_slot_window
990                    ),
991                });
992            }
993            return Ok(());
994        }
995
996        // EMPTY FOUNDED EXTENSION: a row-filtered reduction whose reduced base is
997        // empty (e.g. an unfounded FAEEL self-support rule excluded from the founded
998        // model) legitimately materializes zero output rows. With no candidate output
999        // rows there is no row-specific membership window to cover, so the
1000        // coverage-equality invariant below (which requires a positive output capacity)
1001        // does not apply. This mirrors the `row_filter_count == 0` early-Ok above: an
1002        // all-empty result is sound, not under-coverage.
1003        if final_output_rows == 0 && self.output_row_capacity == 0 {
1004            return Ok(());
1005        }
1006
1007        let covered_row_capacity = checked_sum(
1008            self.row_specific_membership_row_capacity,
1009            self.row_filter_row_capacity_outside_model_slot_window,
1010        )?;
1011        if self.output_row_capacity == 0
1012            || self.row_specific_membership_row_capacity == 0
1013            || covered_row_capacity != self.output_row_capacity
1014        {
1015            return Err(XlogError::UnsupportedEpistemicConstruct {
1016                construct: construct.to_string(),
1017                context: format!(
1018                    "row-filtered final tuple materialization requires GPU row-filter coverage, \
1019                     got row_filters={} final_output_rows={} output_row_capacity={} \
1020                     row_specific_capacity={} fallback_capacity={} model_slots_per_reduction={}",
1021                    self.row_filter_count,
1022                    final_output_rows,
1023                    self.output_row_capacity,
1024                    self.row_specific_membership_row_capacity,
1025                    self.row_filter_row_capacity_outside_model_slot_window,
1026                    self.bounded_model_slots_per_reduction
1027                ),
1028            });
1029        }
1030
1031        let fallback_rows =
1032            final_output_rows.saturating_sub(self.row_specific_membership_row_capacity);
1033        if fallback_rows > self.row_filter_row_capacity_outside_model_slot_window {
1034            return Err(XlogError::UnsupportedEpistemicConstruct {
1035                construct: construct.to_string(),
1036                context: format!(
1037                    "row-filtered final tuple materialization has {} logical rows beyond the \
1038                     row-specific model-slot window but only {} fallback row-filter capacity",
1039                    fallback_rows, self.row_filter_row_capacity_outside_model_slot_window
1040                ),
1041            });
1042        }
1043        Ok(())
1044    }
1045}
1046
1047impl EpistemicGpuTransferBudgetTrace {
1048    /// Build a hot-path transfer trace from provider host-transfer snapshots.
1049    pub fn from_host_transfer_stats(
1050        candidate_count: usize,
1051        before: HostTransferStats,
1052        after: HostTransferStats,
1053    ) -> Result<Self> {
1054        Self::from_host_transfer_stats_with_launch_metadata(
1055            candidate_count,
1056            before,
1057            after,
1058            HostLaunchMetadataTransferStats::default(),
1059            HostLaunchMetadataTransferStats::default(),
1060        )
1061    }
1062
1063    /// Build a hot-path transfer trace while distinguishing bounded launch
1064    /// metadata host-to-device transfers from data-plane transfers.
1065    pub fn from_host_transfer_stats_with_launch_metadata(
1066        candidate_count: usize,
1067        before: HostTransferStats,
1068        after: HostTransferStats,
1069        launch_metadata_before: HostLaunchMetadataTransferStats,
1070        launch_metadata_after: HostLaunchMetadataTransferStats,
1071    ) -> Result<Self> {
1072        require_positive(candidate_count, "epistemic GPU transfer-budget candidates")?;
1073
1074        let tracked_dtoh_bytes =
1075            transfer_counter_delta("dtoh_bytes", before.dtoh_bytes, after.dtoh_bytes)?;
1076        let tracked_data_plane_htod_bytes =
1077            transfer_counter_delta("htod_bytes", before.htod_bytes, after.htod_bytes)?;
1078        let tracked_dtoh_calls =
1079            transfer_counter_delta("dtoh_calls", before.dtoh_calls, after.dtoh_calls)?;
1080        let tracked_data_plane_htod_calls =
1081            transfer_counter_delta("htod_calls", before.htod_calls, after.htod_calls)?;
1082        let tracked_launch_metadata_htod_bytes = transfer_counter_delta(
1083            "launch_metadata_htod_bytes",
1084            launch_metadata_before.htod_bytes,
1085            launch_metadata_after.htod_bytes,
1086        )?;
1087        let tracked_launch_metadata_htod_calls = transfer_counter_delta(
1088            "launch_metadata_htod_calls",
1089            launch_metadata_before.htod_calls,
1090            launch_metadata_after.htod_calls,
1091        )?;
1092        let tracked_aggregate_htod_bytes = tracked_data_plane_htod_bytes
1093            .checked_add(tracked_launch_metadata_htod_bytes)
1094            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
1095                construct: "epistemic GPU transfer budget".to_string(),
1096                context: format!(
1097                    "aggregate H2D bytes overflowed while adding launch metadata: \
1098                     data_plane_htod_bytes={tracked_data_plane_htod_bytes}, \
1099                     launch_metadata_htod_bytes={tracked_launch_metadata_htod_bytes}"
1100                ),
1101            })?;
1102        let tracked_aggregate_htod_calls = tracked_data_plane_htod_calls
1103            .checked_add(tracked_launch_metadata_htod_calls)
1104            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
1105                construct: "epistemic GPU transfer budget".to_string(),
1106                context: format!(
1107                    "aggregate H2D calls overflowed while adding launch metadata: \
1108                     data_plane_htod_calls={tracked_data_plane_htod_calls}, \
1109                     launch_metadata_htod_calls={tracked_launch_metadata_htod_calls}"
1110                ),
1111            })?;
1112
1113        if tracked_launch_metadata_htod_bytes != 0 && tracked_launch_metadata_htod_calls == 0 {
1114            return Err(XlogError::UnsupportedEpistemicConstruct {
1115                construct: "epistemic GPU transfer budget".to_string(),
1116                context: format!(
1117                    "launch metadata H2D bytes require matching H2D calls, got bytes={} calls=0",
1118                    tracked_launch_metadata_htod_bytes
1119                ),
1120            });
1121        }
1122        if tracked_launch_metadata_htod_calls != 0 && tracked_launch_metadata_htod_bytes == 0 {
1123            return Err(XlogError::UnsupportedEpistemicConstruct {
1124                construct: "epistemic GPU transfer budget".to_string(),
1125                context: format!(
1126                    "launch metadata H2D calls require matching payload bytes, got calls={} bytes=0",
1127                    tracked_launch_metadata_htod_calls
1128                ),
1129            });
1130        }
1131
1132        if tracked_dtoh_bytes != 0
1133            || tracked_data_plane_htod_bytes != 0
1134            || tracked_dtoh_calls != 0
1135            || tracked_data_plane_htod_calls != 0
1136        {
1137            return Err(XlogError::UnsupportedEpistemicConstruct {
1138                construct: "epistemic GPU transfer budget".to_string(),
1139                context: format!(
1140                    "tracked host transfer in GPU hot path: tracked data-plane host transfer: \
1141                     dtoh_bytes={tracked_dtoh_bytes}, \
1142                     data_plane_htod_bytes={tracked_data_plane_htod_bytes}, \
1143                     dtoh_calls={tracked_dtoh_calls}, \
1144                     data_plane_htod_calls={tracked_data_plane_htod_calls}, \
1145                     launch_metadata_htod_bytes={tracked_launch_metadata_htod_bytes}, \
1146                     launch_metadata_htod_calls={tracked_launch_metadata_htod_calls}"
1147                ),
1148            });
1149        }
1150
1151        Ok(Self {
1152            candidate_count,
1153            tracked_dtoh_bytes,
1154            tracked_htod_bytes: tracked_data_plane_htod_bytes,
1155            tracked_dtoh_calls,
1156            tracked_htod_calls: tracked_data_plane_htod_calls,
1157            tracked_aggregate_htod_bytes,
1158            tracked_aggregate_htod_calls,
1159            tracked_launch_metadata_htod_bytes,
1160            tracked_launch_metadata_htod_calls,
1161            tracked_data_plane_htod_bytes,
1162            tracked_data_plane_htod_calls,
1163            per_candidate_host_round_trips: 0,
1164        })
1165    }
1166}
1167
1168impl EpistemicGpuFinalResultTransferTrace {
1169    /// Account for the final device-resident output after the hot-path budget window closes.
1170    pub fn from_final_output(
1171        provider: &xlog_cuda::CudaKernelProvider,
1172        final_output: &CudaBuffer,
1173    ) -> Result<Self> {
1174        let row_count_was_cached = final_output.cached_row_count().is_some();
1175        let final_output_rows = provider.device_row_count(final_output)?;
1176        let final_output_column_count = final_output.arity();
1177        let final_output_row_width_bytes = final_output.schema().row_size_bytes();
1178        let final_output_payload_bytes =
1179            checked_product(final_output_rows, final_output_row_width_bytes)? as u64;
1180
1181        Ok(Self {
1182            final_output_rows,
1183            final_output_column_count,
1184            final_output_row_width_bytes,
1185            final_output_payload_bytes,
1186            row_count_device_reads: u32::from(!row_count_was_cached),
1187            tracked_data_plane_dtoh_calls: 0,
1188            tracked_data_plane_dtoh_bytes: 0,
1189        })
1190    }
1191
1192    /// Require retained final-result transfer accounting to match the final device buffer.
1193    pub fn require_matches_final_output(
1194        &self,
1195        construct: &str,
1196        final_output: &CudaBuffer,
1197    ) -> Result<()> {
1198        let Some(cached_rows) = final_output.cached_row_count() else {
1199            return Err(XlogError::UnsupportedEpistemicConstruct {
1200                construct: construct.to_string(),
1201                context:
1202                    "final-result transfer certification requires cached device final row count"
1203                        .to_string(),
1204            });
1205        };
1206        let logical_rows =
1207            validate_logical_row_count(final_output.num_rows(), cached_rows as usize).map_err(
1208                |err| XlogError::UnsupportedEpistemicConstruct {
1209                    construct: construct.to_string(),
1210                    context: format!("invalid final-output logical row count: {err}"),
1211                },
1212            )?;
1213        let row_width = final_output.schema().row_size_bytes();
1214        let payload_bytes = checked_product(logical_rows, row_width)? as u64;
1215        if self.final_output_rows != logical_rows
1216            || self.final_output_column_count != final_output.arity()
1217            || self.final_output_row_width_bytes != row_width
1218            || self.final_output_payload_bytes != payload_bytes
1219        {
1220            return Err(XlogError::UnsupportedEpistemicConstruct {
1221                construct: construct.to_string(),
1222                context: format!(
1223                    "final-result transfer trace does not match final device output: rows={}/{} \
1224                     columns={}/{} row_width={}/{} payload_bytes={}/{}",
1225                    self.final_output_rows,
1226                    logical_rows,
1227                    self.final_output_column_count,
1228                    final_output.arity(),
1229                    self.final_output_row_width_bytes,
1230                    row_width,
1231                    self.final_output_payload_bytes,
1232                    payload_bytes
1233                ),
1234            });
1235        }
1236        if self.row_count_device_reads > 1 {
1237            return Err(XlogError::UnsupportedEpistemicConstruct {
1238                construct: construct.to_string(),
1239                context: format!(
1240                    "final-result transfer reads one device row-count scalar at most, got {}",
1241                    self.row_count_device_reads
1242                ),
1243            });
1244        }
1245
1246        Ok(())
1247    }
1248}
1249
1250impl EpistemicGpuSemanticTrace {
1251    /// Require semantic phase counts to match the retained GPU execution traces.
1252    pub fn require_matches_execution_traces(
1253        &self,
1254        construct: &str,
1255        candidate_generation: &EpistemicGpuCandidateGenerationTrace,
1256        propagation: &EpistemicGpuPropagationTrace,
1257        model_membership: &EpistemicGpuModelMembershipTrace,
1258        world_view_validation: &EpistemicGpuWorldViewValidationTrace,
1259    ) -> Result<()> {
1260        let expected_pruned = self
1261            .generated_candidates
1262            .checked_sub(propagation.propagated_candidates)
1263            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
1264                construct: construct.to_string(),
1265                context: format!(
1266                    "semantic trace phase counts cannot propagate more candidates than were \
1267                     generated: generated={} propagated={}",
1268                    self.generated_candidates, propagation.propagated_candidates
1269                ),
1270            })?;
1271        let expected_reduced_model_slots = checked_product(
1272            checked_product(
1273                world_view_validation.candidates_checked,
1274                model_membership.reduction_count,
1275            )?,
1276            model_membership.models_per_reduction,
1277        )?;
1278        let expected_guesses = checked_product(
1279            candidate_generation.generated_candidates,
1280            candidate_generation.literal_count,
1281        )?;
1282        if self.generated_candidates != candidate_generation.generated_candidates
1283            || self.guesses != expected_guesses
1284            || self.propagated_candidates != propagation.propagated_candidates
1285            || self.pruned_candidates != expected_pruned
1286            || self.tested_candidates != world_view_validation.candidates_checked
1287            || self.reduced_model_slots_checked != expected_reduced_model_slots
1288        {
1289            return Err(XlogError::UnsupportedEpistemicConstruct {
1290                construct: construct.to_string(),
1291                context: format!(
1292                    "semantic trace phase counts must match retained GPU execution traces, got \
1293                     generated={} expected_generated={} guesses={} expected_guesses={} \
1294                     propagated={} expected_propagated={} pruned={} expected_pruned={} \
1295                     tested={} expected_tested={} reduced_model_slots={} \
1296                     expected_reduced_model_slots={}",
1297                    self.generated_candidates,
1298                    candidate_generation.generated_candidates,
1299                    self.guesses,
1300                    expected_guesses,
1301                    self.propagated_candidates,
1302                    propagation.propagated_candidates,
1303                    self.pruned_candidates,
1304                    expected_pruned,
1305                    self.tested_candidates,
1306                    world_view_validation.candidates_checked,
1307                    self.reduced_model_slots_checked,
1308                    expected_reduced_model_slots
1309                ),
1310            });
1311        }
1312
1313        Ok(())
1314    }
1315
1316    /// Require bounded rejection-buffer metadata accounting to match generated candidates.
1317    pub fn require_rejection_metadata_accounting(&self, construct: &str) -> Result<()> {
1318        let expected_metadata_bytes =
1319            checked_product(self.generated_candidates, std::mem::size_of::<u32>())? as u64;
1320        if self.rejection_reason_metadata_bytes != expected_metadata_bytes {
1321            return Err(XlogError::UnsupportedEpistemicConstruct {
1322                construct: construct.to_string(),
1323                context: format!(
1324                    "semantic trace rejection metadata accounting must match the bounded device \
1325                     rejection-buffer read, got bytes={} expected_bytes={}",
1326                    self.rejection_reason_metadata_bytes, expected_metadata_bytes
1327                ),
1328            });
1329        }
1330
1331        Ok(())
1332    }
1333
1334    /// Require accepted/rejected candidate indices to partition generated candidates.
1335    pub fn require_candidate_index_partition(&self, construct: &str) -> Result<()> {
1336        let accounted_candidates = self.accepted_candidates.checked_add(self.rejected_candidates).ok_or_else(|| {
1337            XlogError::UnsupportedEpistemicConstruct {
1338                construct: construct.to_string(),
1339                context: format!(
1340                    "semantic trace candidate index partition accounting overflowed: accepted={} rejected={}",
1341                    self.accepted_candidates, self.rejected_candidates
1342                ),
1343            }
1344        })?;
1345        if self.accepted_candidate_indices.len() != self.accepted_candidates
1346            || self.rejected_candidate_indices.len() != self.rejected_candidates
1347            || self.accepted_world_views != self.accepted_candidates
1348            || accounted_candidates != self.generated_candidates
1349        {
1350            return Err(XlogError::UnsupportedEpistemicConstruct {
1351                construct: construct.to_string(),
1352                context: format!(
1353                    "semantic trace candidate index partition requires counts and index vectors \
1354                     to match generated candidates, got generated={} accepted={} \
1355                     accepted_indices={} accepted_world_views={} rejected={} rejected_indices={}",
1356                    self.generated_candidates,
1357                    self.accepted_candidates,
1358                    self.accepted_candidate_indices.len(),
1359                    self.accepted_world_views,
1360                    self.rejected_candidates,
1361                    self.rejected_candidate_indices.len()
1362                ),
1363            });
1364        }
1365        if self.rejection_reasons.len() != self.rejected_candidates {
1366            return Err(XlogError::UnsupportedEpistemicConstruct {
1367                construct: construct.to_string(),
1368                context: format!(
1369                    "semantic trace rejection reason count must match rejected candidates, got \
1370                     reasons={} rejected={}",
1371                    self.rejection_reasons.len(),
1372                    self.rejected_candidates
1373                ),
1374            });
1375        }
1376        self.typed_rejection_reasons()?;
1377
1378        let mut seen = BTreeSet::new();
1379        for (kind, indices) in [
1380            ("accepted", self.accepted_candidate_indices.as_slice()),
1381            ("rejected", self.rejected_candidate_indices.as_slice()),
1382        ] {
1383            for &index in indices {
1384                if index >= self.generated_candidates {
1385                    return Err(XlogError::UnsupportedEpistemicConstruct {
1386                        construct: construct.to_string(),
1387                        context: format!(
1388                            "semantic trace candidate index partition has out-of-range {kind} \
1389                             index {index} for generated candidate count {}",
1390                            self.generated_candidates
1391                        ),
1392                    });
1393                }
1394                if !seen.insert(index) {
1395                    return Err(XlogError::UnsupportedEpistemicConstruct {
1396                        construct: construct.to_string(),
1397                        context: format!(
1398                            "semantic trace candidate index partition contains duplicate \
1399                             candidate index {index}"
1400                        ),
1401                    });
1402                }
1403            }
1404        }
1405        if seen.len() != self.generated_candidates {
1406            return Err(XlogError::UnsupportedEpistemicConstruct {
1407                construct: construct.to_string(),
1408                context: format!(
1409                    "semantic trace candidate index partition covers {} of {} generated \
1410                     candidates",
1411                    seen.len(),
1412                    self.generated_candidates
1413                ),
1414            });
1415        }
1416
1417        Ok(())
1418    }
1419
1420    /// Decode nonzero device rejection codes into typed GPU semantic reasons.
1421    pub fn typed_rejection_reasons(&self) -> Result<Vec<EpistemicGpuRejectionReason>> {
1422        self.rejection_reasons
1423            .iter()
1424            .copied()
1425            .map(EpistemicGpuRejectionReason::from_code)
1426            .collect()
1427    }
1428
1429    /// Summarize accepted/rejected candidates from the device rejection buffer.
1430    pub fn from_device_rejection_reasons(
1431        provider: &xlog_cuda::CudaKernelProvider,
1432        workspace: &EpistemicGpuWorkspace,
1433        candidate_generation: &EpistemicGpuCandidateGenerationTrace,
1434        propagation: &EpistemicGpuPropagationTrace,
1435        model_membership: &EpistemicGpuModelMembershipTrace,
1436        world_view_validation: &EpistemicGpuWorldViewValidationTrace,
1437    ) -> Result<Self> {
1438        let candidate_count = candidate_generation.generated_candidates;
1439        require_positive(candidate_count, "epistemic GPU semantic-trace candidates")?;
1440        if candidate_count > workspace.layout.rejection_reason_slots {
1441            return Err(XlogError::ResourceExhausted {
1442                context: "epistemic GPU semantic-trace rejection metadata".to_string(),
1443                estimated_bytes: candidate_count as u64,
1444                budget_bytes: workspace.layout.rejection_reason_slots as u64,
1445            });
1446        }
1447        if propagation.literal_count != candidate_generation.literal_count
1448            || model_membership.literal_count != candidate_generation.literal_count
1449            || world_view_validation.literal_count != candidate_generation.literal_count
1450        {
1451            return Err(XlogError::UnsupportedEpistemicConstruct {
1452                construct: "epistemic GPU semantic trace".to_string(),
1453                context: format!(
1454                    "semantic trace requires all GPU stages to agree on literal count, got \
1455                     generated={} propagated={} membership={} validation={}",
1456                    candidate_generation.literal_count,
1457                    propagation.literal_count,
1458                    model_membership.literal_count,
1459                    world_view_validation.literal_count
1460                ),
1461            });
1462        }
1463        let pruned_candidates = candidate_count
1464            .checked_sub(propagation.propagated_candidates)
1465            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
1466                construct: "epistemic GPU semantic trace".to_string(),
1467                context: format!(
1468                    "semantic trace cannot prune more candidates than were generated: \
1469                     generated={} propagated={}",
1470                    candidate_count, propagation.propagated_candidates
1471                ),
1472            })?;
1473        if propagation.rejection_reason_slots_written < candidate_count {
1474            return Err(XlogError::UnsupportedEpistemicConstruct {
1475                construct: "epistemic GPU semantic trace".to_string(),
1476                context: format!(
1477                    "semantic trace requires rejection metadata for every generated candidate, \
1478                     got generated={} rejection_slots_initialized={}",
1479                    candidate_count, propagation.rejection_reason_slots_written
1480                ),
1481            });
1482        }
1483        if model_membership.candidates_checked != candidate_count
1484            || world_view_validation.candidates_checked != candidate_count
1485        {
1486            return Err(XlogError::UnsupportedEpistemicConstruct {
1487                construct: "epistemic GPU semantic trace".to_string(),
1488                context: format!(
1489                    "semantic trace requires GPU validation coverage for every generated \
1490                     candidate, got generated={} membership_checked={} validation_checked={}",
1491                    candidate_count,
1492                    model_membership.candidates_checked,
1493                    world_view_validation.candidates_checked
1494                ),
1495            });
1496        }
1497        if model_membership.reduction_count != world_view_validation.reduction_count
1498            || model_membership.models_per_reduction != world_view_validation.models_per_reduction
1499        {
1500            return Err(XlogError::UnsupportedEpistemicConstruct {
1501                construct: "epistemic GPU semantic trace".to_string(),
1502                context: format!(
1503                    "semantic trace requires model-membership and world-view validation layouts \
1504                     to match, got membership_reductions={} validation_reductions={} \
1505                     membership_models_per_reduction={} validation_models_per_reduction={}",
1506                    model_membership.reduction_count,
1507                    world_view_validation.reduction_count,
1508                    model_membership.models_per_reduction,
1509                    world_view_validation.models_per_reduction
1510                ),
1511            });
1512        }
1513
1514        let raw_rejection_reasons = provider
1515            .dtoh_small_metadata_untracked(&workspace.rejection_reasons, candidate_count)?;
1516        let rejection_reason_metadata_bytes =
1517            checked_product(raw_rejection_reasons.len(), std::mem::size_of::<u32>())? as u64;
1518        // Bounded metadata read of the parallel constraint-violation index buffer.
1519        // Like `rejection_reasons`, this is an untracked post-hot-path metadata
1520        // read, not a data-plane transfer.
1521        let raw_constraint_violation_index = provider.dtoh_small_metadata_untracked(
1522            &workspace.constraint_violation_index,
1523            candidate_count,
1524        )?;
1525        let constraint_violation_code =
1526            EpistemicGpuRejectionReason::WorldViewConstraintViolation.code();
1527        let mut accepted_candidate_indices = Vec::new();
1528        let mut rejected_candidate_indices = Vec::new();
1529        let mut rejection_reasons = Vec::new();
1530        let mut constraint_violation_indices: Vec<Option<u32>> = Vec::new();
1531        for (candidate_index, reason) in raw_rejection_reasons.into_iter().enumerate() {
1532            if reason == 0 {
1533                accepted_candidate_indices.push(candidate_index);
1534            } else {
1535                EpistemicGpuRejectionReason::from_code(reason)?;
1536                rejected_candidate_indices.push(candidate_index);
1537                rejection_reasons.push(reason);
1538                // Gate the constraint-specific index on the integrity-constraint
1539                // reason code: the kernel writes `rejection_reasons[c] = 6` and
1540                // `constraint_violation_index[c] = constraint` together, so the
1541                // index is trustworthy exactly when the reason is 6. Any other
1542                // reason -> None, independent of buffer contents (also defends
1543                // the zero-constraint path where the sentinel is never written).
1544                let firing = raw_constraint_violation_index
1545                    .get(candidate_index)
1546                    .copied()
1547                    .unwrap_or(u32::MAX);
1548                if reason == constraint_violation_code && firing != u32::MAX {
1549                    constraint_violation_indices.push(Some(firing));
1550                } else {
1551                    constraint_violation_indices.push(None);
1552                }
1553            }
1554        }
1555        let accepted_candidates = accepted_candidate_indices.len();
1556        let rejected_candidates = rejection_reasons.len();
1557        let reduced_model_slots_checked = checked_product(
1558            checked_product(
1559                world_view_validation.candidates_checked,
1560                model_membership.reduction_count,
1561            )?,
1562            model_membership.models_per_reduction,
1563        )?;
1564        Ok(Self {
1565            generated_candidates: candidate_count,
1566            guesses: checked_product(candidate_count, candidate_generation.literal_count)?,
1567            propagated_candidates: propagation.propagated_candidates,
1568            pruned_candidates,
1569            tested_candidates: world_view_validation.candidates_checked,
1570            reduced_model_slots_checked,
1571            accepted_candidates,
1572            accepted_candidate_indices,
1573            accepted_world_views: accepted_candidates,
1574            rejected_candidates,
1575            rejected_candidate_indices,
1576            rejection_reasons,
1577            constraint_violation_indices,
1578            rejection_reason_metadata_bytes,
1579        })
1580    }
1581}
1582
1583fn transfer_counter_delta(name: &str, before: u64, after: u64) -> Result<u64> {
1584    after
1585        .checked_sub(before)
1586        .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
1587            construct: "epistemic GPU transfer budget".to_string(),
1588            context: format!(
1589                "host transfer counter decreased during GPU hot path: {name} before={before}, \
1590                 after={after}"
1591            ),
1592        })
1593}
1594
1595impl EpistemicGpuModelMembershipTrace {
1596    /// Build a model-membership trace for a bounded device launch.
1597    pub fn for_counts(
1598        literal_count: usize,
1599        candidate_count: usize,
1600        reduction_count: usize,
1601        models_per_reduction: usize,
1602    ) -> Result<Self> {
1603        require_positive(literal_count, "epistemic GPU model-membership literals")?;
1604        require_positive(candidate_count, "epistemic GPU model-membership candidates")?;
1605        require_positive(reduction_count, "epistemic GPU model-membership reductions")?;
1606        require_positive(
1607            models_per_reduction,
1608            "epistemic GPU model-membership models",
1609        )?;
1610        let model_membership_bytes_written = checked_product(
1611            checked_product(
1612                checked_product(candidate_count, reduction_count)?,
1613                models_per_reduction,
1614            )?,
1615            literal_count,
1616        )?;
1617        require_u32_launch_bound(
1618            model_membership_bytes_written,
1619            "epistemic GPU model-membership launch",
1620        )?;
1621
1622        Ok(Self {
1623            literal_count,
1624            candidates_checked: candidate_count,
1625            reduction_count,
1626            models_per_reduction,
1627            model_membership_bytes_written,
1628            output_row_count_device_reads: 1,
1629            tuple_source_row_count_device_reads: 0,
1630            tuple_source_key_column_device_reads: 0,
1631            rejection_reason_slots_checked: candidate_count,
1632            membership_source: EpistemicGpuModelMembershipSource::ReducedOutputRowCountOnly,
1633            kernel_launches: 1,
1634            host_write_ops: 0,
1635            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
1636        })
1637    }
1638
1639    /// Build a model-membership trace backed by reduced stable-model tuple sources.
1640    pub fn for_stable_model_tuple_sources(
1641        literal_count: usize,
1642        candidate_count: usize,
1643        reduction_count: usize,
1644        models_per_reduction: usize,
1645        tuple_source_count: usize,
1646    ) -> Result<Self> {
1647        Self::for_stable_model_tuple_sources_with_key_columns(
1648            literal_count,
1649            candidate_count,
1650            reduction_count,
1651            models_per_reduction,
1652            tuple_source_count,
1653            0,
1654        )
1655    }
1656
1657    /// Build a model-membership trace backed by tuple sources and key columns.
1658    pub fn for_stable_model_tuple_sources_with_key_columns(
1659        literal_count: usize,
1660        candidate_count: usize,
1661        reduction_count: usize,
1662        models_per_reduction: usize,
1663        tuple_source_count: usize,
1664        tuple_source_key_column_count: usize,
1665    ) -> Result<Self> {
1666        require_positive(literal_count, "epistemic GPU model-membership literals")?;
1667        require_positive(candidate_count, "epistemic GPU model-membership candidates")?;
1668        require_positive(reduction_count, "epistemic GPU model-membership reductions")?;
1669        require_positive(
1670            models_per_reduction,
1671            "epistemic GPU model-membership models",
1672        )?;
1673        require_positive(
1674            tuple_source_count,
1675            "epistemic GPU model-membership tuple sources",
1676        )?;
1677        if tuple_source_count > u32::MAX as usize {
1678            return Err(XlogError::ResourceExhausted {
1679                context: "epistemic GPU model-membership tuple sources".to_string(),
1680                estimated_bytes: tuple_source_count as u64,
1681                budget_bytes: u32::MAX as u64,
1682            });
1683        }
1684        if tuple_source_key_column_count > u32::MAX as usize {
1685            return Err(XlogError::ResourceExhausted {
1686                context: "epistemic GPU model-membership tuple key columns".to_string(),
1687                estimated_bytes: tuple_source_key_column_count as u64,
1688                budget_bytes: u32::MAX as u64,
1689            });
1690        }
1691        let model_membership_bytes_written = checked_product(
1692            checked_product(
1693                checked_product(candidate_count, reduction_count)?,
1694                models_per_reduction,
1695            )?,
1696            literal_count,
1697        )?;
1698        require_u32_launch_bound(
1699            model_membership_bytes_written,
1700            "epistemic GPU model-membership launch",
1701        )?;
1702
1703        Ok(Self {
1704            literal_count,
1705            candidates_checked: candidate_count,
1706            reduction_count,
1707            models_per_reduction,
1708            model_membership_bytes_written,
1709            output_row_count_device_reads: 0,
1710            tuple_source_row_count_device_reads: tuple_source_count as u32,
1711            tuple_source_key_column_device_reads: tuple_source_key_column_count as u32,
1712            rejection_reason_slots_checked: candidate_count,
1713            membership_source: EpistemicGpuModelMembershipSource::StableModelTupleBuffer,
1714            kernel_launches: tuple_source_count as u32,
1715            host_write_ops: 0,
1716            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
1717        })
1718    }
1719
1720    /// Attach CUDA-event timing captured by the runtime launch path.
1721    pub const fn with_kernel_timing(
1722        mut self,
1723        kernel_timing: EpistemicGpuKernelTimingTrace,
1724    ) -> Self {
1725        self.kernel_timing = kernel_timing;
1726        self
1727    }
1728
1729    /// Require semantic stable-model tuple membership before accepting execution.
1730    pub fn require_stable_model_tuple_source(&self) -> Result<()> {
1731        if self.membership_source != EpistemicGpuModelMembershipSource::StableModelTupleBuffer {
1732            return Err(XlogError::UnsupportedEpistemicConstruct {
1733                construct: "epistemic GPU stable-model membership certification".to_string(),
1734                context: format!(
1735                    "model-membership source {:?} is bounded staging only; actual reduced \
1736                     stable-model tuple membership is required before returning accepted \
1737                     epistemic execution",
1738                    self.membership_source
1739                ),
1740            });
1741        }
1742
1743        Ok(())
1744    }
1745
1746    /// Require the tuple-key device reads planned for this model-membership trace.
1747    pub fn require_planned_tuple_key_column_reads(
1748        &self,
1749        expected_key_column_reads: usize,
1750    ) -> Result<()> {
1751        if self.tuple_source_key_column_device_reads as usize != expected_key_column_reads {
1752            return Err(XlogError::UnsupportedEpistemicConstruct {
1753                construct: "epistemic GPU stable-model membership certification".to_string(),
1754                context: format!(
1755                    "model-membership tuple-key device column reads must match the planned \
1756                     nonzero-arity tuple keys, got reads={} expected={}",
1757                    self.tuple_source_key_column_device_reads, expected_key_column_reads
1758                ),
1759            });
1760        }
1761
1762        Ok(())
1763    }
1764}
1765
1766impl EpistemicGpuWorldViewValidationTrace {
1767    /// Build a world-view validation trace for a bounded device launch.
1768    pub fn for_counts(
1769        literal_count: usize,
1770        candidate_count: usize,
1771        reduction_count: usize,
1772        models_per_reduction: usize,
1773    ) -> Result<Self> {
1774        require_positive(
1775            literal_count,
1776            "epistemic GPU world-view validation literals",
1777        )?;
1778        require_positive(
1779            candidate_count,
1780            "epistemic GPU world-view validation candidates",
1781        )?;
1782        require_positive(
1783            reduction_count,
1784            "epistemic GPU world-view validation reductions",
1785        )?;
1786        require_positive(
1787            models_per_reduction,
1788            "epistemic GPU world-view validation models",
1789        )?;
1790        let model_membership_bytes_checked = checked_product(
1791            checked_product(
1792                checked_product(candidate_count, reduction_count)?,
1793                models_per_reduction,
1794            )?,
1795            literal_count,
1796        )?;
1797        require_u32_launch_bound(
1798            model_membership_bytes_checked,
1799            "epistemic GPU world-view validation membership launch",
1800        )?;
1801
1802        Ok(Self {
1803            literal_count,
1804            candidates_checked: candidate_count,
1805            reduction_count,
1806            models_per_reduction,
1807            model_membership_bytes_checked,
1808            world_view_slots_checked: candidate_count,
1809            rejection_reason_slots_written: candidate_count,
1810            kernel_launches: 1,
1811            host_write_ops: 0,
1812            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
1813        })
1814    }
1815
1816    /// Attach CUDA-event timing captured by the runtime launch path.
1817    pub const fn with_kernel_timing(
1818        mut self,
1819        kernel_timing: EpistemicGpuKernelTimingTrace,
1820    ) -> Self {
1821        self.kernel_timing = kernel_timing;
1822        self
1823    }
1824}
1825
1826impl EpistemicGpuPropagationTrace {
1827    /// Build a propagation trace for a bounded device launch.
1828    pub fn for_counts(literal_count: usize, candidate_count: usize) -> Result<Self> {
1829        require_positive(literal_count, "epistemic GPU propagation literals")?;
1830        require_positive(candidate_count, "epistemic GPU propagation candidates")?;
1831        require_u32_launch_dimensions(
1832            &[literal_count, candidate_count],
1833            "epistemic GPU propagation launch",
1834        )?;
1835
1836        Ok(Self {
1837            literal_count,
1838            propagated_candidates: candidate_count,
1839            world_view_bytes_written: candidate_count,
1840            rejection_reason_slots_written: candidate_count,
1841            kernel_launches: 1,
1842            host_write_ops: 0,
1843            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
1844        })
1845    }
1846
1847    /// Attach CUDA-event timing captured by the runtime launch path.
1848    pub const fn with_kernel_timing(
1849        mut self,
1850        kernel_timing: EpistemicGpuKernelTimingTrace,
1851    ) -> Self {
1852        self.kernel_timing = kernel_timing;
1853        self
1854    }
1855}
1856
1857impl EpistemicGpuWorkspaceResetTrace {
1858    /// Build the reset trace implied by a workspace layout.
1859    pub fn for_layout(layout: EpistemicGpuWorkspaceLayout) -> Self {
1860        Self::try_for_layout(layout)
1861            .expect("epistemic GPU workspace reset trace byte total overflowed")
1862    }
1863
1864    /// Build the reset trace implied by a workspace layout, failing closed on overflow.
1865    pub fn try_for_layout(layout: EpistemicGpuWorkspaceLayout) -> Result<Self> {
1866        Ok(Self {
1867            candidate_assumption_bytes: layout.candidate_assumption_bytes,
1868            world_view_bytes: layout.world_view_bytes,
1869            model_membership_bytes: layout.model_membership_bytes,
1870            rejection_reason_bytes: checked_product(
1871                layout.rejection_reason_slots,
1872                std::mem::size_of::<u32>(),
1873            )?,
1874            device_zero_ops: 4,
1875            host_write_ops: 0,
1876        })
1877    }
1878
1879    /// Total bytes zeroed by the reset path.
1880    pub fn total_zeroed_bytes(&self) -> usize {
1881        self.try_total_zeroed_bytes()
1882            .expect("epistemic GPU workspace reset byte total overflowed")
1883    }
1884
1885    /// Checked total bytes zeroed by the reset path.
1886    pub fn try_total_zeroed_bytes(&self) -> Result<usize> {
1887        checked_sum(
1888            checked_sum(
1889                checked_sum(self.candidate_assumption_bytes, self.world_view_bytes)?,
1890                self.model_membership_bytes,
1891            )?,
1892            self.rejection_reason_bytes,
1893        )
1894    }
1895
1896    /// Require the retained reset trace to match the prepared workspace layout.
1897    pub fn require_matches_layout(
1898        &self,
1899        construct: &str,
1900        layout: EpistemicGpuWorkspaceLayout,
1901    ) -> Result<()> {
1902        let expected = Self::try_for_layout(layout)?;
1903        if *self != expected {
1904            return Err(XlogError::UnsupportedEpistemicConstruct {
1905                construct: construct.to_string(),
1906                context: format!(
1907                    "workspace reset trace does not match prepared GPU workspace layout: \
1908                     candidate_bytes={}/{} world_view_bytes={}/{} model_membership_bytes={}/{} \
1909                     rejection_reason_bytes={}/{} device_zero_ops={}/{} host_write_ops={}/{}",
1910                    self.candidate_assumption_bytes,
1911                    expected.candidate_assumption_bytes,
1912                    self.world_view_bytes,
1913                    expected.world_view_bytes,
1914                    self.model_membership_bytes,
1915                    expected.model_membership_bytes,
1916                    self.rejection_reason_bytes,
1917                    expected.rejection_reason_bytes,
1918                    self.device_zero_ops,
1919                    expected.device_zero_ops,
1920                    self.host_write_ops,
1921                    expected.host_write_ops
1922                ),
1923            });
1924        }
1925
1926        Ok(())
1927    }
1928}
1929
1930/// Runtime preflight summary for an epistemic executable plan.
1931#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1932pub struct EpistemicGpuRuntimePreflight {
1933    /// Selected epistemic semantics mode for the accepted GPU execution.
1934    pub epistemic_mode: EirEpistemicMode,
1935    /// Production execution backend copied from the semantic plan.
1936    pub execution_backend: EpistemicExecutionBackend,
1937    /// Unsupported-shape behavior copied from the semantic plan.
1938    pub fallback_policy: EpistemicFallbackPolicy,
1939    /// GPU workspace layout required by the executable plan.
1940    pub workspace_layout: EpistemicGpuWorkspaceLayout,
1941    /// Compiled reduced-runtime rule count.
1942    pub reduced_runtime_rule_count: usize,
1943    /// Compiler-generated reduced integrity-constraint relations to validate.
1944    pub reduced_constraint_relation_count: usize,
1945    /// Reduced rules that the epistemic planner marked as requiring WCOJ eligibility.
1946    pub wcoj_required_reduction_count: usize,
1947    /// Number of reduced rules carrying a `MultiWayJoin` route.
1948    pub multiway_reduction_count: usize,
1949    /// Number of K-clique WCOJ plans reused from the production planner.
1950    pub kclique_wcoj_plan_count: usize,
1951    /// Number of triangle WCOJ routes reused from the production runtime.
1952    pub wcoj_triangle_route_count: usize,
1953    /// Number of 4-cycle WCOJ routes reused from the production runtime.
1954    pub wcoj_4cycle_route_count: usize,
1955    /// Generic Free Join multiway routes: `plan: None` nodes
1956    /// matching no dedicated kernel shape. Opportunistic by contract —
1957    /// the dispatcher may structurally decline to the embedded binary
1958    /// fallback (non-prefix bound columns, non-u32 inputs, …), so these
1959    /// routes carry no hard dispatch obligation and are excluded from
1960    /// the dedicated-WCOJ certification arithmetic.
1961    pub free_join_route_count: usize,
1962    /// K-clique WCOJ plan counts by arity K=5..8.
1963    pub kclique_wcoj_plan_count_by_arity: [usize; 4],
1964    /// Maximum K-clique arity observed across production WCOJ plans.
1965    pub kclique_wcoj_max_arity: u8,
1966    /// Live edge-permutation slots carried by production K-clique plans.
1967    pub kclique_wcoj_edge_permutation_count: usize,
1968    /// Distinct K-clique stream groups carried by production WCOJ plans.
1969    pub kclique_stream_group_count: usize,
1970    /// K-clique WCOJ plans carrying helper-split skew scheduling metadata.
1971    pub kclique_skew_scheduled_plan_count: usize,
1972    /// Number of structured planned-hash routes.
1973    pub planned_hash_route_count: usize,
1974    /// Planned-hash routes where complete planner costs predicted hash wins.
1975    pub planned_hash_planner_wins_count: usize,
1976    /// Planned-hash routes selected because complete WCOJ stats were unavailable.
1977    pub planned_hash_incomplete_stats_count: usize,
1978    /// Planned-hash routes carrying finite hash-vs-WCOJ cost evidence.
1979    pub planned_hash_cost_evidence_count: usize,
1980    /// Sorted-layout edge-slot requirements carried by WCOJ plans.
1981    pub sorted_layout_requirement_count: usize,
1982    /// Helper-splitting specs carried by WCOJ plans.
1983    pub helper_split_spec_count: usize,
1984    /// Compiler-created helper-split relation rules in the reduced runtime plan.
1985    pub helper_relation_rule_count: usize,
1986    /// WCOJ input scans of compiler-created helper-split relations.
1987    pub helper_relation_scan_count: usize,
1988    /// Tuple-membership bindings certified for stable-model membership checks.
1989    pub tuple_membership_binding_count: usize,
1990    /// Solver assumption bindings exported by the semantic plan.
1991    pub solver_assumption_binding_count: usize,
1992    /// Solver production capabilities required by the semantic plan.
1993    pub solver_required_capability_count: usize,
1994    /// Distinct solver statuses required by the semantic plan.
1995    pub solver_required_status_count: usize,
1996    /// Non-negated `know` operators represented by the executable GPU plan.
1997    pub know_operator_count: usize,
1998    /// Non-negated `possible` operators represented by the executable GPU plan.
1999    pub possible_operator_count: usize,
2000    /// Negated `know` operators represented as `not know`.
2001    pub not_know_operator_count: usize,
2002    /// Negated `possible` operators represented as `not possible`.
2003    pub not_possible_operator_count: usize,
2004}
2005
2006impl EpistemicGpuRuntimePreflight {
2007    /// Whether this accepted execution used Gelfond-1991 (G91) compatibility semantics.
2008    pub fn is_g91_mode(&self) -> bool {
2009        matches!(self.epistemic_mode, EirEpistemicMode::G91)
2010    }
2011
2012    /// Whether this accepted execution used default FAEEL semantics.
2013    pub fn is_faeel_mode(&self) -> bool {
2014        matches!(self.epistemic_mode, EirEpistemicMode::Faeel)
2015    }
2016
2017    /// Inspect an executable epistemic plan before GPU kernel dispatch.
2018    pub fn for_executable_plan(
2019        executable: &EpistemicExecutablePlan,
2020        capacities: EpistemicGpuWorkspaceCapacities,
2021    ) -> Result<Self> {
2022        executable.gpu_plan.validate_tuple_membership_bindings()?;
2023        executable.gpu_plan.validate_solver_contract()?;
2024        // A plan may carry MULTIPLE epistemic output heads: a JOINT-SOLVED
2025        // coalesced multi-head component shares ONE candidate enumeration +
2026        // world-view validation and materializes each head against the shared
2027        // accepted world view (see `execute_epistemic_gpu_execution`). Soundness of
2028        // the coupling is gated in the logic lowering
2029        // (`classify_cross_component_modal_coupling`); the runtime executes the
2030        // resulting well-formed plan and is no longer restricted to one head.
2031        require_epistemic_gpu_kernel_phases(&executable.gpu_plan)?;
2032        require_epistemic_gpu_buffer_contract(&executable.gpu_plan)?;
2033
2034        let workspace_layout =
2035            EpistemicGpuWorkspaceLayout::for_plan(&executable.gpu_plan, capacities)?;
2036        let mut routes = RuntimeRouteSummary::default();
2037        let mut reduced_runtime_rule_count = 0usize;
2038        let mut reduced_constraint_relation_names = Vec::new();
2039        let wcoj_required_reduction_count = executable
2040            .gpu_plan
2041            .reductions
2042            .iter()
2043            .filter(|reduction| {
2044                matches!(
2045                    reduction.wcoj_status,
2046                    EpistemicWcojReductionStatus::RequiresPlannerEligibility
2047                )
2048            })
2049            .count();
2050        let helper_relation_ids = helper_relation_ids(executable);
2051        let mut helper_relation_rule_count = 0usize;
2052        let mut helper_relation_scan_count = 0usize;
2053
2054        for rule in executable
2055            .reduced_runtime_plan
2056            .rules_by_scc
2057            .iter()
2058            .flatten()
2059        {
2060            reduced_runtime_rule_count += 1;
2061            if rule.head.starts_with(XLOG_CONSTRAINT_RELATION_PREFIX)
2062                && !reduced_constraint_relation_names
2063                    .iter()
2064                    .any(|name| name == &rule.head)
2065            {
2066                reduced_constraint_relation_names.push(rule.head.as_str());
2067            }
2068            if rule.head.starts_with("__kclique_helper_") {
2069                helper_relation_rule_count += 1;
2070            }
2071            helper_relation_scan_count +=
2072                count_helper_relation_scans(&rule.body, &helper_relation_ids);
2073            summarize_runtime_routes(&rule.body, &mut routes);
2074        }
2075
2076        if wcoj_required_reduction_count > routes.multiway_reduction_count {
2077            return Err(XlogError::UnsupportedEpistemicConstruct {
2078                construct: "epistemic GPU WCOJ route certification".to_string(),
2079                context: format!(
2080                    "plan requires {} WCOJ-eligible epistemic reductions, but reduced runtime \
2081                     plan exposes {} MultiWayJoin routes",
2082                    wcoj_required_reduction_count, routes.multiway_reduction_count
2083                ),
2084            });
2085        }
2086
2087        let planned_hash_reason_count = routes
2088            .planned_hash_planner_wins_count
2089            .checked_add(routes.planned_hash_incomplete_stats_count)
2090            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
2091                construct: "epistemic GPU planned-hash certification".to_string(),
2092                context: "planned-hash reason counters overflowed".to_string(),
2093            })?;
2094        if planned_hash_reason_count != routes.planned_hash_route_count
2095            || routes.planned_hash_cost_evidence_count < routes.planned_hash_planner_wins_count
2096        {
2097            return Err(XlogError::UnsupportedEpistemicConstruct {
2098                construct: "epistemic GPU planned-hash certification".to_string(),
2099                context: format!(
2100                    "planned_hash_routes={}, planner_wins={}, incomplete_stats={}, \
2101                     finite_cost_evidence={}",
2102                    routes.planned_hash_route_count,
2103                    routes.planned_hash_planner_wins_count,
2104                    routes.planned_hash_incomplete_stats_count,
2105                    routes.planned_hash_cost_evidence_count
2106                ),
2107            });
2108        }
2109
2110        if routes.kclique_wcoj_plan_count > 0 && routes.kclique_wcoj_edge_permutation_count == 0 {
2111            return Err(XlogError::UnsupportedEpistemicConstruct {
2112                construct: "epistemic GPU K-clique WCOJ certification".to_string(),
2113                context: format!(
2114                    "K-clique WCOJ plans require live edge-permutation slots, got \
2115                     kclique_plans={} edge_permutation_slots=0",
2116                    routes.kclique_wcoj_plan_count
2117                ),
2118            });
2119        }
2120
2121        if routes.helper_split_spec_count > 0
2122            && (helper_relation_rule_count < routes.helper_split_spec_count
2123                || helper_relation_scan_count < routes.helper_split_spec_count)
2124        {
2125            return Err(XlogError::UnsupportedEpistemicConstruct {
2126                construct: "epistemic GPU helper-split certification".to_string(),
2127                context: format!(
2128                    "helper_split_specs={}, helper_relation_rules={}, \
2129                     helper_relation_scans={}",
2130                    routes.helper_split_spec_count,
2131                    helper_relation_rule_count,
2132                    helper_relation_scan_count
2133                ),
2134            });
2135        }
2136
2137        let mut know_operator_count = 0usize;
2138        let mut possible_operator_count = 0usize;
2139        let mut not_know_operator_count = 0usize;
2140        let mut not_possible_operator_count = 0usize;
2141        for literal in &executable.gpu_plan.epistemic_literals {
2142            match (literal.op, literal.negated) {
2143                (EirEpistemicOp::Know, false) => know_operator_count += 1,
2144                (EirEpistemicOp::Possible, false) => possible_operator_count += 1,
2145                (EirEpistemicOp::Know, true) => not_know_operator_count += 1,
2146                (EirEpistemicOp::Possible, true) => not_possible_operator_count += 1,
2147            }
2148        }
2149
2150        Ok(Self {
2151            epistemic_mode: executable.gpu_plan.mode,
2152            execution_backend: executable.gpu_plan.execution_backend,
2153            fallback_policy: executable.gpu_plan.fallback_policy,
2154            workspace_layout,
2155            reduced_runtime_rule_count,
2156            reduced_constraint_relation_count: reduced_constraint_relation_names.len(),
2157            wcoj_required_reduction_count,
2158            multiway_reduction_count: routes.multiway_reduction_count,
2159            kclique_wcoj_plan_count: routes.kclique_wcoj_plan_count,
2160            wcoj_triangle_route_count: routes.wcoj_triangle_route_count,
2161            wcoj_4cycle_route_count: routes.wcoj_4cycle_route_count,
2162            free_join_route_count: routes.free_join_route_count,
2163            kclique_wcoj_plan_count_by_arity: routes.kclique_wcoj_plan_count_by_arity,
2164            kclique_wcoj_max_arity: routes.kclique_wcoj_max_arity,
2165            kclique_wcoj_edge_permutation_count: routes.kclique_wcoj_edge_permutation_count,
2166            kclique_stream_group_count: routes.kclique_stream_groups.len(),
2167            kclique_skew_scheduled_plan_count: routes.kclique_skew_scheduled_plan_count,
2168            planned_hash_route_count: routes.planned_hash_route_count,
2169            planned_hash_planner_wins_count: routes.planned_hash_planner_wins_count,
2170            planned_hash_incomplete_stats_count: routes.planned_hash_incomplete_stats_count,
2171            planned_hash_cost_evidence_count: routes.planned_hash_cost_evidence_count,
2172            sorted_layout_requirement_count: routes.sorted_layout_requirement_count,
2173            helper_split_spec_count: routes.helper_split_spec_count,
2174            helper_relation_rule_count,
2175            helper_relation_scan_count,
2176            tuple_membership_binding_count: executable.gpu_plan.tuple_membership_bindings.len(),
2177            solver_assumption_binding_count: executable
2178                .gpu_plan
2179                .solver_contract
2180                .assumption_bindings
2181                .len(),
2182            solver_required_capability_count: executable
2183                .gpu_plan
2184                .solver_contract
2185                .distinct_required_capability_count(),
2186            solver_required_status_count: executable
2187                .gpu_plan
2188                .solver_contract
2189                .distinct_required_status_count(),
2190            know_operator_count,
2191            possible_operator_count,
2192            not_know_operator_count,
2193            not_possible_operator_count,
2194        })
2195    }
2196}
2197
2198/// Prepared runtime state for epistemic GPU execution.
2199pub struct EpistemicGpuPreparedExecution {
2200    /// Static preflight summary.
2201    pub preflight: EpistemicGpuRuntimePreflight,
2202    /// Planned tuple-membership bindings certified before GPU execution.
2203    pub tuple_membership_bindings: Vec<EpistemicTupleMembershipBinding>,
2204    /// Device-resident workspace buffers.
2205    pub workspace: EpistemicGpuWorkspace,
2206    /// Device-side initialization trace for the workspace buffers.
2207    pub workspace_reset: EpistemicGpuWorkspaceResetTrace,
2208}
2209
2210/// Counter trace captured around a reduced production runtime dispatch.
2211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2212pub struct EpistemicGpuRuntimeTrace {
2213    /// Static preflight summary for the executed plan.
2214    pub preflight: EpistemicGpuRuntimePreflight,
2215    /// Runtime counters before dispatch.
2216    pub counters_before: EpistemicGpuRuntimeCounters,
2217    /// Runtime counters after dispatch.
2218    pub counters_after: EpistemicGpuRuntimeCounters,
2219    /// Checked counter delta for the dispatch window.
2220    pub counter_delta: EpistemicGpuRuntimeCounters,
2221    /// WCOJ certification result derived from preflight obligations and deltas.
2222    pub wcoj_certification: EpistemicGpuRuntimeWcojCertification,
2223}
2224
2225impl EpistemicGpuRuntimeTrace {
2226    /// Build a trace from static preflight data and runtime counter snapshots.
2227    pub fn from_preflight_and_counters(
2228        preflight: EpistemicGpuRuntimePreflight,
2229        counters_before: EpistemicGpuRuntimeCounters,
2230        counters_after: EpistemicGpuRuntimeCounters,
2231    ) -> Self {
2232        Self::try_from_preflight_and_counters(preflight, counters_before, counters_after)
2233            .expect("runtime counter snapshots must be monotonic")
2234    }
2235
2236    /// Build a trace from static preflight data and runtime counter snapshots, failing closed
2237    /// if runtime proof counters move backwards or overflow while being summarized.
2238    pub fn try_from_preflight_and_counters(
2239        preflight: EpistemicGpuRuntimePreflight,
2240        counters_before: EpistemicGpuRuntimeCounters,
2241        counters_after: EpistemicGpuRuntimeCounters,
2242    ) -> Result<Self> {
2243        let counter_delta = counters_after.checked_delta_since(counters_before)?;
2244        let wcoj_certification = EpistemicGpuRuntimeWcojCertification::try_for_preflight_and_delta(
2245            &preflight,
2246            &counter_delta,
2247        )?;
2248
2249        Ok(Self {
2250            preflight,
2251            counters_before,
2252            counters_after,
2253            counter_delta,
2254            wcoj_certification,
2255        })
2256    }
2257
2258    /// Fail closed when a WCOJ-required epistemic reduction lacks runtime evidence.
2259    pub fn require_wcoj_certification(&self) -> Result<()> {
2260        match self.wcoj_certification {
2261            EpistemicGpuRuntimeWcojCertification::MissingRequiredWcojDispatch {
2262                required_multiway_reductions,
2263                required_kclique_plans,
2264                observed_wcoj_dispatches,
2265                observed_kclique_dispatches,
2266            } => Err(XlogError::UnsupportedEpistemicConstruct {
2267                construct: "epistemic GPU WCOJ dispatch certification".to_string(),
2268                context: format!(
2269                    "required_multiway_reductions={required_multiway_reductions}, \
2270                     required_kclique_plans={required_kclique_plans}, \
2271                     observed_wcoj_dispatches={observed_wcoj_dispatches}, \
2272                     observed_kclique_dispatches={observed_kclique_dispatches}"
2273                ),
2274            }),
2275            EpistemicGpuRuntimeWcojCertification::MissingRequiredWcojLayout {
2276                required_sorted_layouts,
2277                observed_layout_events,
2278            } => Err(XlogError::UnsupportedEpistemicConstruct {
2279                construct: "epistemic GPU WCOJ layout certification".to_string(),
2280                context: format!(
2281                    "required_sorted_layouts={required_sorted_layouts}, \
2282                     observed_layout_events={observed_layout_events}"
2283                ),
2284            }),
2285            EpistemicGpuRuntimeWcojCertification::MissingRequiredKcliqueMetadata {
2286                required_kclique_plans,
2287                observed_metadata_builds,
2288                observed_metadata_build_nanos,
2289            } => Err(XlogError::UnsupportedEpistemicConstruct {
2290                construct: "epistemic GPU K-clique metadata certification".to_string(),
2291                context: format!(
2292                    "required_kclique_plans={required_kclique_plans}, \
2293                     observed_metadata_builds={observed_metadata_builds}, \
2294                     observed_metadata_build_nanos={observed_metadata_build_nanos}"
2295                ),
2296            }),
2297            EpistemicGpuRuntimeWcojCertification::NotRequired { .. }
2298            | EpistemicGpuRuntimeWcojCertification::Certified { .. } => Ok(()),
2299        }
2300    }
2301}
2302
2303/// Runtime counters relevant to epistemic GPU certification.
2304#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
2305pub struct EpistemicGpuRuntimeCounters {
2306    /// Successful triangle WCOJ dispatches installed by the executor.
2307    pub wcoj_triangle_dispatch_count: u64,
2308    /// Successful 4-cycle WCOJ dispatches installed by the executor.
2309    pub wcoj_4cycle_dispatch_count: u64,
2310    /// Successful chain dispatches installed by the executor.
2311    pub chain_dispatch_count: u64,
2312    /// Successful K=5 clique WCOJ dispatches installed by the executor.
2313    pub wcoj_clique5_dispatch_count: u64,
2314    /// Successful K=6 clique WCOJ dispatches installed by the executor.
2315    pub wcoj_clique6_dispatch_count: u64,
2316    /// Successful K=7 clique WCOJ dispatches installed by the executor.
2317    pub wcoj_clique7_dispatch_count: u64,
2318    /// Successful K=8 clique WCOJ dispatches installed by the executor.
2319    pub wcoj_clique8_dispatch_count: u64,
2320    /// Successful generic Free Join dispatches installed by the
2321    /// executor. Observability only: free-join routes carry no hard
2322    /// dispatch obligation (structural declines execute the embedded
2323    /// binary fallback by contract), so this counter never gates
2324    /// certification.
2325    pub free_join_dispatch_count: u64,
2326    /// D3 — successful factorized recursive-delta dispatches installed
2327    /// by the executor. Observability only, same contract as the Free
2328    /// Join counter: declines execute the legacy semi-naive path, so
2329    /// this counter never gates certification.
2330    pub factorized_delta_dispatch_count: u64,
2331    /// Provider-level HG triangle dispatch counter.
2332    pub provider_wcoj_triangle_hg_dispatch_count: u64,
2333    /// WCOJ layout-sort invocations observed by the provider.
2334    pub wcoj_layout_sort_invocation_count: u64,
2335    /// WCOJ layout fast-path hits observed by the provider.
2336    pub wcoj_layout_fast_path_hit_count: u64,
2337    /// K-clique metadata builds observed by the provider.
2338    pub kclique_metadata_build_count: u64,
2339    /// Provider-observed nanoseconds spent building K-clique metadata.
2340    pub kclique_metadata_build_nanos: u64,
2341    /// Recursive Merge-phase K-clique histogram refresh boundaries observed by the executor.
2342    pub kclique_histogram_refresh_count: u64,
2343    /// Recursive Merge-phase K-clique histogram refresh accounting time observed by the executor.
2344    pub kclique_histogram_refresh_nanos: u128,
2345}
2346
2347impl EpistemicGpuRuntimeCounters {
2348    fn checked_counter_delta(counter: &str, after: u64, before: u64) -> Result<u64> {
2349        after
2350            .checked_sub(before)
2351            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
2352                construct: "epistemic GPU runtime counter trace".to_string(),
2353                context: format!(
2354                    "runtime proof counter {counter} decreased from {before} to {after}"
2355                ),
2356            })
2357    }
2358
2359    fn checked_counter_delta_u128(counter: &str, after: u128, before: u128) -> Result<u128> {
2360        after
2361            .checked_sub(before)
2362            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
2363                construct: "epistemic GPU runtime counter trace".to_string(),
2364                context: format!(
2365                    "runtime proof counter {counter} decreased from {before} to {after}"
2366                ),
2367            })
2368    }
2369
2370    fn checked_counter_sum(counter: &str, values: &[u64]) -> Result<u64> {
2371        values.iter().try_fold(0u64, |acc, value| {
2372            acc.checked_add(*value)
2373                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
2374                    construct: "epistemic GPU runtime counter trace".to_string(),
2375                    context: format!(
2376                        "runtime proof counter {counter} overflowed while adding {value} to {acc}"
2377                    ),
2378                })
2379        })
2380    }
2381
2382    /// Checked delta from an earlier snapshot.
2383    pub fn checked_delta_since(self, before: Self) -> Result<Self> {
2384        Ok(Self {
2385            wcoj_triangle_dispatch_count: Self::checked_counter_delta(
2386                "wcoj_triangle_dispatch_count",
2387                self.wcoj_triangle_dispatch_count,
2388                before.wcoj_triangle_dispatch_count,
2389            )?,
2390            wcoj_4cycle_dispatch_count: Self::checked_counter_delta(
2391                "wcoj_4cycle_dispatch_count",
2392                self.wcoj_4cycle_dispatch_count,
2393                before.wcoj_4cycle_dispatch_count,
2394            )?,
2395            chain_dispatch_count: Self::checked_counter_delta(
2396                "chain_dispatch_count",
2397                self.chain_dispatch_count,
2398                before.chain_dispatch_count,
2399            )?,
2400            wcoj_clique5_dispatch_count: Self::checked_counter_delta(
2401                "wcoj_clique5_dispatch_count",
2402                self.wcoj_clique5_dispatch_count,
2403                before.wcoj_clique5_dispatch_count,
2404            )?,
2405            wcoj_clique6_dispatch_count: Self::checked_counter_delta(
2406                "wcoj_clique6_dispatch_count",
2407                self.wcoj_clique6_dispatch_count,
2408                before.wcoj_clique6_dispatch_count,
2409            )?,
2410            wcoj_clique7_dispatch_count: Self::checked_counter_delta(
2411                "wcoj_clique7_dispatch_count",
2412                self.wcoj_clique7_dispatch_count,
2413                before.wcoj_clique7_dispatch_count,
2414            )?,
2415            wcoj_clique8_dispatch_count: Self::checked_counter_delta(
2416                "wcoj_clique8_dispatch_count",
2417                self.wcoj_clique8_dispatch_count,
2418                before.wcoj_clique8_dispatch_count,
2419            )?,
2420            factorized_delta_dispatch_count: Self::checked_counter_delta(
2421                "factorized_delta_dispatch_count",
2422                self.factorized_delta_dispatch_count,
2423                before.factorized_delta_dispatch_count,
2424            )?,
2425            free_join_dispatch_count: Self::checked_counter_delta(
2426                "free_join_dispatch_count",
2427                self.free_join_dispatch_count,
2428                before.free_join_dispatch_count,
2429            )?,
2430            provider_wcoj_triangle_hg_dispatch_count: Self::checked_counter_delta(
2431                "provider_wcoj_triangle_hg_dispatch_count",
2432                self.provider_wcoj_triangle_hg_dispatch_count,
2433                before.provider_wcoj_triangle_hg_dispatch_count,
2434            )?,
2435            wcoj_layout_sort_invocation_count: Self::checked_counter_delta(
2436                "wcoj_layout_sort_invocation_count",
2437                self.wcoj_layout_sort_invocation_count,
2438                before.wcoj_layout_sort_invocation_count,
2439            )?,
2440            wcoj_layout_fast_path_hit_count: Self::checked_counter_delta(
2441                "wcoj_layout_fast_path_hit_count",
2442                self.wcoj_layout_fast_path_hit_count,
2443                before.wcoj_layout_fast_path_hit_count,
2444            )?,
2445            kclique_metadata_build_count: Self::checked_counter_delta(
2446                "kclique_metadata_build_count",
2447                self.kclique_metadata_build_count,
2448                before.kclique_metadata_build_count,
2449            )?,
2450            kclique_metadata_build_nanos: Self::checked_counter_delta(
2451                "kclique_metadata_build_nanos",
2452                self.kclique_metadata_build_nanos,
2453                before.kclique_metadata_build_nanos,
2454            )?,
2455            kclique_histogram_refresh_count: Self::checked_counter_delta(
2456                "kclique_histogram_refresh_count",
2457                self.kclique_histogram_refresh_count,
2458                before.kclique_histogram_refresh_count,
2459            )?,
2460            kclique_histogram_refresh_nanos: Self::checked_counter_delta_u128(
2461                "kclique_histogram_refresh_nanos",
2462                self.kclique_histogram_refresh_nanos,
2463                before.kclique_histogram_refresh_nanos,
2464            )?,
2465        })
2466    }
2467
2468    /// Saturating delta from an earlier snapshot.
2469    pub fn saturating_delta_since(self, before: Self) -> Self {
2470        Self {
2471            wcoj_triangle_dispatch_count: self
2472                .wcoj_triangle_dispatch_count
2473                .saturating_sub(before.wcoj_triangle_dispatch_count),
2474            wcoj_4cycle_dispatch_count: self
2475                .wcoj_4cycle_dispatch_count
2476                .saturating_sub(before.wcoj_4cycle_dispatch_count),
2477            chain_dispatch_count: self
2478                .chain_dispatch_count
2479                .saturating_sub(before.chain_dispatch_count),
2480            wcoj_clique5_dispatch_count: self
2481                .wcoj_clique5_dispatch_count
2482                .saturating_sub(before.wcoj_clique5_dispatch_count),
2483            wcoj_clique6_dispatch_count: self
2484                .wcoj_clique6_dispatch_count
2485                .saturating_sub(before.wcoj_clique6_dispatch_count),
2486            wcoj_clique7_dispatch_count: self
2487                .wcoj_clique7_dispatch_count
2488                .saturating_sub(before.wcoj_clique7_dispatch_count),
2489            wcoj_clique8_dispatch_count: self
2490                .wcoj_clique8_dispatch_count
2491                .saturating_sub(before.wcoj_clique8_dispatch_count),
2492            free_join_dispatch_count: self
2493                .free_join_dispatch_count
2494                .saturating_sub(before.free_join_dispatch_count),
2495            factorized_delta_dispatch_count: self
2496                .factorized_delta_dispatch_count
2497                .saturating_sub(before.factorized_delta_dispatch_count),
2498            provider_wcoj_triangle_hg_dispatch_count: self
2499                .provider_wcoj_triangle_hg_dispatch_count
2500                .saturating_sub(before.provider_wcoj_triangle_hg_dispatch_count),
2501            wcoj_layout_sort_invocation_count: self
2502                .wcoj_layout_sort_invocation_count
2503                .saturating_sub(before.wcoj_layout_sort_invocation_count),
2504            wcoj_layout_fast_path_hit_count: self
2505                .wcoj_layout_fast_path_hit_count
2506                .saturating_sub(before.wcoj_layout_fast_path_hit_count),
2507            kclique_metadata_build_count: self
2508                .kclique_metadata_build_count
2509                .saturating_sub(before.kclique_metadata_build_count),
2510            kclique_metadata_build_nanos: self
2511                .kclique_metadata_build_nanos
2512                .saturating_sub(before.kclique_metadata_build_nanos),
2513            kclique_histogram_refresh_count: self
2514                .kclique_histogram_refresh_count
2515                .saturating_sub(before.kclique_histogram_refresh_count),
2516            kclique_histogram_refresh_nanos: self
2517                .kclique_histogram_refresh_nanos
2518                .saturating_sub(before.kclique_histogram_refresh_nanos),
2519        }
2520    }
2521
2522    /// Total WCOJ dispatches installed by the executor.
2523    pub fn wcoj_dispatch_count(&self) -> u64 {
2524        self.wcoj_triangle_dispatch_count
2525            .saturating_add(self.wcoj_4cycle_dispatch_count)
2526            .saturating_add(self.wcoj_clique_dispatch_count())
2527    }
2528
2529    /// Checked total WCOJ dispatches installed by the executor.
2530    pub fn checked_wcoj_dispatch_count(&self) -> Result<u64> {
2531        Self::checked_counter_sum(
2532            "wcoj_dispatch_count",
2533            &[
2534                self.wcoj_triangle_dispatch_count,
2535                self.wcoj_4cycle_dispatch_count,
2536                self.checked_wcoj_clique_dispatch_count()?,
2537            ],
2538        )
2539    }
2540
2541    /// Total K-clique WCOJ dispatches installed by the executor.
2542    pub fn wcoj_clique_dispatch_count(&self) -> u64 {
2543        self.wcoj_clique5_dispatch_count
2544            .saturating_add(self.wcoj_clique6_dispatch_count)
2545            .saturating_add(self.wcoj_clique7_dispatch_count)
2546            .saturating_add(self.wcoj_clique8_dispatch_count)
2547    }
2548
2549    /// Checked total K-clique WCOJ dispatches installed by the executor.
2550    pub fn checked_wcoj_clique_dispatch_count(&self) -> Result<u64> {
2551        Self::checked_counter_sum(
2552            "wcoj_clique_dispatch_count",
2553            &[
2554                self.wcoj_clique5_dispatch_count,
2555                self.wcoj_clique6_dispatch_count,
2556                self.wcoj_clique7_dispatch_count,
2557                self.wcoj_clique8_dispatch_count,
2558            ],
2559        )
2560    }
2561}
2562
2563/// WCOJ certification status for an epistemic runtime dispatch attempt.
2564#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2565pub enum EpistemicGpuRuntimeWcojCertification {
2566    /// The preflight did not require a WCOJ dispatch.
2567    NotRequired {
2568        /// Observed executor-installed WCOJ dispatches.
2569        observed_wcoj_dispatches: u64,
2570        /// Structured planned-hash routes that replaced WCOJ dispatch obligations.
2571        planned_hash_routes: usize,
2572        /// Planned-hash routes where complete planner costs predicted hash wins.
2573        planned_hash_planner_wins: usize,
2574        /// Planned-hash routes selected because complete WCOJ stats were unavailable.
2575        planned_hash_incomplete_stats: usize,
2576        /// Planned-hash routes carrying finite hash-vs-WCOJ cost evidence.
2577        planned_hash_cost_evidence: usize,
2578    },
2579    /// Runtime counters prove the required WCOJ dispatch happened.
2580    Certified {
2581        /// Observed executor-installed WCOJ dispatches.
2582        observed_wcoj_dispatches: u64,
2583        /// MultiWayJoin reductions certified by the observed WCOJ dispatches.
2584        certified_multiway_reductions: usize,
2585        /// Observed executor-installed K-clique dispatches.
2586        observed_kclique_dispatches: u64,
2587        /// Edge-permutation slots certified by the dispatched K-clique plans.
2588        certified_edge_permutation_slots: usize,
2589        /// Distinct stream groups certified by the dispatched K-clique plans.
2590        certified_stream_groups: usize,
2591        /// Helper-split skew-scheduled K-clique plans certified by dispatch.
2592        certified_skew_scheduled_plans: usize,
2593        /// Sorted-layout requirements certified by the dispatched K-clique plans.
2594        certified_sorted_layout_requirements: usize,
2595        /// Helper-split specs certified by the dispatched K-clique plans.
2596        certified_helper_split_specs: usize,
2597        /// Helper relation rules proving production helper-split rewrite happened.
2598        certified_helper_relation_rules: usize,
2599        /// Helper relation scans proving WCOJ consumed production helper output.
2600        certified_helper_relation_scans: usize,
2601        /// Observed provider WCOJ layout-sort invocations.
2602        observed_layout_sorts: u64,
2603        /// Observed provider WCOJ layout fast-path hits.
2604        observed_layout_fast_path_hits: u64,
2605        /// Observed provider K-clique metadata builds.
2606        observed_metadata_builds: u64,
2607        /// Observed provider time spent building K-clique metadata.
2608        observed_metadata_build_nanos: u64,
2609        /// Observed recursive K-clique histogram refresh boundaries.
2610        observed_histogram_refreshes: u64,
2611        /// Observed recursive K-clique histogram refresh accounting time.
2612        observed_histogram_refresh_nanos: u128,
2613    },
2614    /// The plan required sorted layouts, but no layout path executed.
2615    MissingRequiredWcojLayout {
2616        /// Sorted-layout requirements found during preflight.
2617        required_sorted_layouts: usize,
2618        /// Observed layout sort or fast-path events.
2619        observed_layout_events: u64,
2620    },
2621    /// The plan dispatched a K-clique WCOJ route, but metadata-build counters did not advance.
2622    MissingRequiredKcliqueMetadata {
2623        /// K-clique WCOJ plans found during preflight.
2624        required_kclique_plans: usize,
2625        /// Observed provider K-clique metadata builds.
2626        observed_metadata_builds: u64,
2627        /// Observed provider time spent building K-clique metadata.
2628        observed_metadata_build_nanos: u64,
2629    },
2630    /// The plan had WCOJ obligations, but counters did not advance.
2631    MissingRequiredWcojDispatch {
2632        /// MultiWayJoin reductions found during preflight after excluding planned hash routes.
2633        required_multiway_reductions: usize,
2634        /// K-clique WCOJ plans found during preflight.
2635        required_kclique_plans: usize,
2636        /// Observed executor-installed WCOJ dispatches.
2637        observed_wcoj_dispatches: u64,
2638        /// Observed executor-installed K-clique dispatches.
2639        observed_kclique_dispatches: u64,
2640    },
2641}
2642
2643/// CUDA provider identity that produced an epistemic GPU execution result.
2644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2645pub struct EpistemicGpuProviderIdentity {
2646    /// CUDA device ordinal used by the executor.
2647    pub device_ordinal: usize,
2648    /// Stable address of the executor's CUDA device wrapper.
2649    pub device_ptr: usize,
2650    /// Stable address of the executor's GPU memory manager.
2651    pub memory_ptr: usize,
2652}
2653
2654impl EpistemicGpuProviderIdentity {
2655    /// Capture the device and memory-manager identity for a CUDA provider.
2656    pub fn from_provider(provider: &xlog_cuda::CudaKernelProvider) -> Self {
2657        Self {
2658            device_ordinal: provider.device().ordinal(),
2659            device_ptr: Arc::as_ptr(provider.device()) as usize,
2660            memory_ptr: Arc::as_ptr(provider.memory()) as usize,
2661        }
2662    }
2663}
2664
2665/// Output from executing the reduced production runtime plan for an epistemic program.
2666pub struct EpistemicGpuExecutionResult {
2667    /// CUDA provider identity that owns this result's device-resident buffers.
2668    pub provider_identity: EpistemicGpuProviderIdentity,
2669    /// Prepared workspace and preflight state.
2670    pub prepared: EpistemicGpuPreparedExecution,
2671    /// Candidate-generation trace captured before reduced-plan dispatch.
2672    pub candidate_generation: EpistemicGpuCandidateGenerationTrace,
2673    /// Candidate-propagation trace captured before reduced-plan dispatch.
2674    pub propagation: EpistemicGpuPropagationTrace,
2675    /// Candidate-validation trace captured before reduced-plan dispatch.
2676    pub candidate_validation: EpistemicGpuCandidateValidationTrace,
2677    /// Model-membership staging trace captured after reduced-plan dispatch.
2678    pub model_membership: EpistemicGpuModelMembershipTrace,
2679    /// World-view validation trace captured after model-membership staging.
2680    pub world_view_validation: EpistemicGpuWorldViewValidationTrace,
2681    /// World-view integrity-constraint validation trace captured after world-view validation.
2682    pub constraint_world_view_validation: EpistemicGpuConstraintWorldViewValidationTrace,
2683    /// Accepted-candidate materialization trace captured after world-view validation.
2684    pub materialization: EpistemicGpuMaterializationTrace,
2685    /// Final result materialization trace captured from reduced output metadata.
2686    pub final_result_materialization: EpistemicGpuFinalResultMaterializationTrace,
2687    /// Final query tuple materialization trace captured after final-result gating.
2688    pub final_tuple_materialization: EpistemicGpuFinalTupleMaterializationTrace,
2689    /// Hot-path host-transfer budget trace for epistemic GPU execution.
2690    pub transfer_budget: EpistemicGpuTransferBudgetTrace,
2691    /// Final-result transfer accounting after the GPU hot path.
2692    pub final_result_transfer: EpistemicGpuFinalResultTransferTrace,
2693    /// Reduced integrity-constraint validation after production runtime dispatch.
2694    pub constraint_validation: EpistemicGpuConstraintValidationTrace,
2695    /// Device-derived semantic summary after world-view validation.
2696    pub semantic_trace: EpistemicGpuSemanticTrace,
2697    /// Tuple-membership bindings that were validated and executed for this result.
2698    pub tuple_membership_bindings: Vec<EpistemicTupleMembershipBinding>,
2699    /// Device-resident final query output buffer.
2700    ///
2701    /// For a single epistemic output head this is the only materialized relation.
2702    /// For a JOINT-SOLVED coalesced multi-head component this is the PRIMARY head's
2703    /// output (the last reduction's head); the remaining coupled heads, each
2704    /// materialized against the SAME accepted world view, are in
2705    /// [`Self::additional_head_outputs`].
2706    pub final_output: CudaBuffer,
2707    /// Additional coupled-head outputs for a JOINT-SOLVED multi-head component.
2708    ///
2709    /// Empty for single-head execution. Each entry is `(head_predicate, buffer)`
2710    /// for a distinct epistemic output head OTHER than the primary head, filtered
2711    /// against the shared accepted world view via that head's row-filter bindings.
2712    pub additional_head_outputs: Vec<(String, CudaBuffer)>,
2713    /// Device-resident final tuple evidence buffer before public projection.
2714    pub tuple_evidence_output: Option<CudaBuffer>,
2715    /// Output buffer returned by the reduced production execution plan.
2716    pub output: CudaBuffer,
2717    /// Runtime counter trace for the reduced production plan dispatch.
2718    pub trace: EpistemicGpuRuntimeTrace,
2719}
2720
2721impl EpistemicGpuExecutionResult {
2722    /// Device-resident output used to derive concrete tuple-membership evidence.
2723    pub fn tuple_evidence_output(&self) -> &CudaBuffer {
2724        self.tuple_evidence_output
2725            .as_ref()
2726            .unwrap_or(&self.final_output)
2727    }
2728
2729    /// Require that the retained runtime trace certifies the prepared execution.
2730    pub fn require_runtime_dispatch_certification(&self) -> Result<()> {
2731        if self.trace.preflight != self.prepared.preflight {
2732            return Err(XlogError::UnsupportedEpistemicConstruct {
2733                construct: "epistemic GPU runtime dispatch certification".to_string(),
2734                context: "runtime trace preflight does not match prepared execution preflight"
2735                    .to_string(),
2736            });
2737        }
2738        if self.prepared.workspace.layout != self.prepared.preflight.workspace_layout {
2739            return Err(XlogError::UnsupportedEpistemicConstruct {
2740                construct: "epistemic GPU runtime dispatch certification".to_string(),
2741                context: "prepared GPU workspace layout does not match preflight workspace layout"
2742                    .to_string(),
2743            });
2744        }
2745        self.prepared
2746            .workspace
2747            .require_buffer_lengths_match_layout("epistemic GPU runtime dispatch certification")?;
2748        if self.tuple_membership_bindings.len()
2749            != self.prepared.preflight.tuple_membership_binding_count
2750        {
2751            return Err(XlogError::UnsupportedEpistemicConstruct {
2752                construct: "epistemic GPU runtime dispatch certification".to_string(),
2753                context: format!(
2754                    "runtime tuple-membership bindings do not match prepared preflight, got {} \
2755                     bindings for preflight count {}",
2756                    self.tuple_membership_bindings.len(),
2757                    self.prepared.preflight.tuple_membership_binding_count
2758                ),
2759            });
2760        }
2761        if self.tuple_membership_bindings != self.prepared.tuple_membership_bindings {
2762            return Err(XlogError::UnsupportedEpistemicConstruct {
2763                construct: "epistemic GPU runtime dispatch certification".to_string(),
2764                context: "runtime tuple-membership bindings do not match prepared GPU execution"
2765                    .to_string(),
2766            });
2767        }
2768        self.model_membership
2769            .require_planned_tuple_key_column_reads(expected_tuple_key_column_reads(
2770                &self.prepared.tuple_membership_bindings,
2771            )?)?;
2772        self.prepared.workspace_reset.require_matches_layout(
2773            "epistemic GPU runtime dispatch certification",
2774            self.prepared.preflight.workspace_layout,
2775        )?;
2776        self.final_result_transfer.require_matches_final_output(
2777            "epistemic GPU runtime dispatch certification",
2778            &self.final_output,
2779        )?;
2780        self.constraint_validation.require_matches_preflight(
2781            "epistemic GPU runtime dispatch certification",
2782            &self.prepared.preflight,
2783        )?;
2784        self.candidate_validation
2785            .require_matches_candidate_generation(
2786                "epistemic GPU runtime dispatch certification",
2787                &self.candidate_generation,
2788            )?;
2789        self.semantic_trace.require_matches_execution_traces(
2790            "epistemic GPU runtime dispatch certification",
2791            &self.candidate_generation,
2792            &self.propagation,
2793            &self.model_membership,
2794            &self.world_view_validation,
2795        )?;
2796        self.semantic_trace.require_rejection_metadata_accounting(
2797            "epistemic GPU runtime dispatch certification",
2798        )?;
2799        self.semantic_trace
2800            .require_candidate_index_partition("epistemic GPU runtime dispatch certification")?;
2801        let aggregate_kernel_timing = self.try_aggregate_kernel_timing()?;
2802        if !aggregate_kernel_timing.is_recorded() {
2803            return Err(XlogError::UnsupportedEpistemicConstruct {
2804                construct: "epistemic GPU runtime dispatch certification".to_string(),
2805                context: "accepted GPU execution did not record CUDA-event timing".to_string(),
2806            });
2807        }
2808        self.trace.require_wcoj_certification()
2809    }
2810
2811    /// Aggregate CUDA-event timing from all epistemic GPU hot-path kernels.
2812    pub fn aggregate_kernel_timing(&self) -> EpistemicGpuKernelTimingTrace {
2813        self.try_aggregate_kernel_timing()
2814            .expect("epistemic GPU kernel timing aggregation overflowed")
2815    }
2816
2817    /// Checked CUDA-event timing aggregation for certification paths.
2818    pub fn try_aggregate_kernel_timing(&self) -> Result<EpistemicGpuKernelTimingTrace> {
2819        let traces = [
2820            self.candidate_generation.kernel_timing,
2821            self.propagation.kernel_timing,
2822            self.candidate_validation.kernel_timing,
2823            self.model_membership.kernel_timing,
2824            self.world_view_validation.kernel_timing,
2825            self.materialization.kernel_timing,
2826            self.final_result_materialization.kernel_timing,
2827            self.final_tuple_materialization.kernel_timing,
2828        ];
2829
2830        if traces
2831            .iter()
2832            .all(EpistemicGpuKernelTimingTrace::is_recorded)
2833        {
2834            EpistemicGpuKernelTimingTrace::checked_sum(traces)
2835        } else {
2836            Ok(EpistemicGpuKernelTimingTrace::unrecorded())
2837        }
2838    }
2839}
2840
2841/// Batch-level trace proving split components reused the single-plan GPU path.
2842#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2843pub struct EpistemicGpuBatchExecutionTrace {
2844    /// Number of executable components requested by the batch.
2845    pub component_count: usize,
2846    /// Number of components executed through `execute_epistemic_gpu_execution`.
2847    pub gpu_runtime_component_executions: usize,
2848    /// Hot-path device-to-host calls tracked across all components.
2849    pub tracked_dtoh_calls: u64,
2850    /// Hot-path data-plane host-to-device calls tracked across all components.
2851    pub tracked_htod_calls: u64,
2852    /// Hot-path aggregate host-to-device calls tracked across all components.
2853    pub tracked_aggregate_htod_calls: u64,
2854    /// Hot-path launch-metadata host-to-device calls tracked across all components.
2855    pub tracked_launch_metadata_htod_calls: u64,
2856    /// Hot-path data-plane host-to-device calls tracked across all components.
2857    pub tracked_data_plane_htod_calls: u64,
2858    /// Per-candidate host round trips tracked across all components.
2859    pub per_candidate_host_round_trips: u64,
2860    /// Final output rows represented across all component device buffers.
2861    pub final_output_rows: usize,
2862    /// Final output payload bytes represented across all component device buffers.
2863    pub final_output_payload_bytes: u64,
2864    /// Device row-count metadata reads used for component final-result accounting.
2865    pub final_result_row_count_device_reads: u32,
2866    /// Post-hot-path final-result data-plane device-to-host calls across all components.
2867    pub final_result_data_plane_dtoh_calls: u64,
2868    /// Post-hot-path final-result data-plane device-to-host bytes across all components.
2869    pub final_result_data_plane_dtoh_bytes: u64,
2870    /// Reduced integrity-constraint relations checked across all components.
2871    pub checked_constraint_relations: usize,
2872    /// Reduced integrity-constraint relations with violating rows across all components.
2873    pub violated_constraint_relations: usize,
2874    /// Constraint row-count metadata reads used across all components.
2875    pub constraint_row_count_device_reads: u32,
2876    /// Accepted world views observed across component semantic traces.
2877    pub accepted_world_views: usize,
2878    /// Rejected candidates observed across component semantic traces.
2879    pub rejected_candidates: usize,
2880    /// Non-negated `know` operators observed across component preflight traces.
2881    pub know_operator_count: usize,
2882    /// Non-negated `possible` operators observed across component preflight traces.
2883    pub possible_operator_count: usize,
2884    /// Negated `know` operators observed as `not know` across component preflight traces.
2885    pub not_know_operator_count: usize,
2886    /// Negated `possible` operators observed as `not possible` across component preflight traces.
2887    pub not_possible_operator_count: usize,
2888    /// Aggregate CUDA-event timing from all component hot-path kernels.
2889    pub aggregate_kernel_timing: EpistemicGpuKernelTimingTrace,
2890}
2891
2892impl EpistemicGpuBatchExecutionTrace {
2893    /// Build an aggregate trace from completed component results.
2894    pub fn from_component_results(results: &[EpistemicGpuExecutionResult]) -> Self {
2895        Self::try_from_component_results(results)
2896            .expect("epistemic GPU batch trace aggregation overflowed")
2897    }
2898
2899    /// Build an aggregate trace from completed component results and fail closed
2900    /// if any certification counter overflows.
2901    pub fn try_from_component_results(results: &[EpistemicGpuExecutionResult]) -> Result<Self> {
2902        let component_kernel_timings = results
2903            .iter()
2904            .map(EpistemicGpuExecutionResult::try_aggregate_kernel_timing)
2905            .collect::<Result<Vec<_>>>()?;
2906        let aggregate_kernel_timing = if component_kernel_timings
2907            .iter()
2908            .all(EpistemicGpuKernelTimingTrace::is_recorded)
2909        {
2910            EpistemicGpuKernelTimingTrace::checked_sum(component_kernel_timings)
2911        } else {
2912            Ok(EpistemicGpuKernelTimingTrace::unrecorded())
2913        };
2914        let aggregate_kernel_timing = aggregate_kernel_timing?;
2915
2916        Ok(Self {
2917            component_count: results.len(),
2918            gpu_runtime_component_executions: results.len(),
2919            tracked_dtoh_calls: checked_batch_sum_u64(
2920                "tracked_dtoh_calls",
2921                results
2922                    .iter()
2923                    .map(|result| result.transfer_budget.tracked_dtoh_calls),
2924            )?,
2925            tracked_htod_calls: checked_batch_sum_u64(
2926                "tracked_htod_calls",
2927                results
2928                    .iter()
2929                    .map(|result| result.transfer_budget.tracked_htod_calls),
2930            )?,
2931            tracked_aggregate_htod_calls: checked_batch_sum_u64(
2932                "tracked_aggregate_htod_calls",
2933                results
2934                    .iter()
2935                    .map(|result| result.transfer_budget.tracked_aggregate_htod_calls),
2936            )?,
2937            tracked_launch_metadata_htod_calls: checked_batch_sum_u64(
2938                "tracked_launch_metadata_htod_calls",
2939                results
2940                    .iter()
2941                    .map(|result| result.transfer_budget.tracked_launch_metadata_htod_calls),
2942            )?,
2943            tracked_data_plane_htod_calls: checked_batch_sum_u64(
2944                "tracked_data_plane_htod_calls",
2945                results
2946                    .iter()
2947                    .map(|result| result.transfer_budget.tracked_data_plane_htod_calls),
2948            )?,
2949            per_candidate_host_round_trips: checked_batch_sum_u64(
2950                "per_candidate_host_round_trips",
2951                results
2952                    .iter()
2953                    .map(|result| result.transfer_budget.per_candidate_host_round_trips),
2954            )?,
2955            final_output_rows: checked_batch_sum_usize(
2956                "final_output_rows",
2957                results
2958                    .iter()
2959                    .map(|result| result.final_result_transfer.final_output_rows),
2960            )?,
2961            final_output_payload_bytes: checked_batch_sum_u64(
2962                "final_output_payload_bytes",
2963                results
2964                    .iter()
2965                    .map(|result| result.final_result_transfer.final_output_payload_bytes),
2966            )?,
2967            final_result_row_count_device_reads: checked_batch_sum_u32(
2968                "final_result_row_count_device_reads",
2969                results
2970                    .iter()
2971                    .map(|result| result.final_result_transfer.row_count_device_reads),
2972            )?,
2973            final_result_data_plane_dtoh_calls: checked_batch_sum_u64(
2974                "final_result_data_plane_dtoh_calls",
2975                results
2976                    .iter()
2977                    .map(|result| result.final_result_transfer.tracked_data_plane_dtoh_calls),
2978            )?,
2979            final_result_data_plane_dtoh_bytes: checked_batch_sum_u64(
2980                "final_result_data_plane_dtoh_bytes",
2981                results
2982                    .iter()
2983                    .map(|result| result.final_result_transfer.tracked_data_plane_dtoh_bytes),
2984            )?,
2985            checked_constraint_relations: checked_batch_sum_usize(
2986                "checked_constraint_relations",
2987                results
2988                    .iter()
2989                    .map(|result| result.constraint_validation.checked_constraint_relations),
2990            )?,
2991            violated_constraint_relations: checked_batch_sum_usize(
2992                "violated_constraint_relations",
2993                results
2994                    .iter()
2995                    .map(|result| result.constraint_validation.violated_constraint_relations),
2996            )?,
2997            constraint_row_count_device_reads: checked_batch_sum_u32(
2998                "constraint_row_count_device_reads",
2999                results
3000                    .iter()
3001                    .map(|result| result.constraint_validation.row_count_device_reads),
3002            )?,
3003            accepted_world_views: checked_batch_sum_usize(
3004                "accepted_world_views",
3005                results
3006                    .iter()
3007                    .map(|result| result.semantic_trace.accepted_world_views),
3008            )?,
3009            rejected_candidates: checked_batch_sum_usize(
3010                "rejected_candidates",
3011                results
3012                    .iter()
3013                    .map(|result| result.semantic_trace.rejected_candidates),
3014            )?,
3015            know_operator_count: checked_batch_sum_usize(
3016                "know_operator_count",
3017                results
3018                    .iter()
3019                    .map(|result| result.prepared.preflight.know_operator_count),
3020            )?,
3021            possible_operator_count: checked_batch_sum_usize(
3022                "possible_operator_count",
3023                results
3024                    .iter()
3025                    .map(|result| result.prepared.preflight.possible_operator_count),
3026            )?,
3027            not_know_operator_count: checked_batch_sum_usize(
3028                "not_know_operator_count",
3029                results
3030                    .iter()
3031                    .map(|result| result.prepared.preflight.not_know_operator_count),
3032            )?,
3033            not_possible_operator_count: checked_batch_sum_usize(
3034                "not_possible_operator_count",
3035                results
3036                    .iter()
3037                    .map(|result| result.prepared.preflight.not_possible_operator_count),
3038            )?,
3039            aggregate_kernel_timing,
3040        })
3041    }
3042}
3043
3044fn checked_batch_sum_u64(
3045    counter: &'static str,
3046    values: impl IntoIterator<Item = u64>,
3047) -> Result<u64> {
3048    values.into_iter().try_fold(0u64, |acc, value| {
3049        acc.checked_add(value)
3050            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
3051                construct: "epistemic GPU batch execution trace".to_string(),
3052                context: format!(
3053                    "batch counter {counter} overflowed while aggregating component traces: \
3054                     acc={acc} next={value}"
3055                ),
3056            })
3057    })
3058}
3059
3060fn checked_batch_sum_u32(
3061    counter: &'static str,
3062    values: impl IntoIterator<Item = u32>,
3063) -> Result<u32> {
3064    values.into_iter().try_fold(0u32, |acc, value| {
3065        acc.checked_add(value)
3066            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
3067                construct: "epistemic GPU batch execution trace".to_string(),
3068                context: format!(
3069                    "batch counter {counter} overflowed while aggregating component traces: \
3070                     acc={acc} next={value}"
3071                ),
3072            })
3073    })
3074}
3075
3076fn checked_batch_sum_usize(
3077    counter: &'static str,
3078    values: impl IntoIterator<Item = usize>,
3079) -> Result<usize> {
3080    values.into_iter().try_fold(0usize, |acc, value| {
3081        acc.checked_add(value)
3082            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
3083                construct: "epistemic GPU batch execution trace".to_string(),
3084                context: format!(
3085                    "batch counter {counter} overflowed while aggregating component traces: \
3086                     acc={acc} next={value}"
3087                ),
3088            })
3089    })
3090}
3091
3092/// Results plus aggregate trace from a split/batch epistemic GPU execution.
3093pub struct EpistemicGpuBatchExecutionResult {
3094    /// Per-component execution results from the existing single-plan GPU path.
3095    pub results: Vec<EpistemicGpuExecutionResult>,
3096    /// Aggregate batch certification trace.
3097    pub trace: EpistemicGpuBatchExecutionTrace,
3098}
3099
3100impl EpistemicGpuBatchExecutionResult {
3101    /// Require the retained aggregate trace to be derived from the component results.
3102    pub fn require_trace_matches_components(&self, construct: &str) -> Result<()> {
3103        if self.results.is_empty() {
3104            return Err(XlogError::UnsupportedEpistemicConstruct {
3105                construct: construct.to_string(),
3106                context: "batch evidence requires at least one GPU component".to_string(),
3107            });
3108        }
3109        let expected = EpistemicGpuBatchExecutionTrace::try_from_component_results(&self.results)?;
3110        if self.trace != expected {
3111            return Err(XlogError::UnsupportedEpistemicConstruct {
3112                construct: construct.to_string(),
3113                context: format!(
3114                    "batch aggregate trace does not match component GPU execution results: \
3115                     trace_components={}/{} expected_components={}/{} \
3116                     trace_final_rows={} expected_final_rows={} trace_dtoh_calls={} \
3117                     expected_dtoh_calls={} trace_data_plane_htod_calls={} \
3118                     expected_data_plane_htod_calls={} trace_constraint_violations={} \
3119                     expected_constraint_violations={} trace_accepted_world_views={} \
3120                     expected_accepted_world_views={}",
3121                    self.trace.gpu_runtime_component_executions,
3122                    self.trace.component_count,
3123                    expected.gpu_runtime_component_executions,
3124                    expected.component_count,
3125                    self.trace.final_output_rows,
3126                    expected.final_output_rows,
3127                    self.trace.tracked_dtoh_calls,
3128                    expected.tracked_dtoh_calls,
3129                    self.trace.tracked_data_plane_htod_calls,
3130                    expected.tracked_data_plane_htod_calls,
3131                    self.trace.violated_constraint_relations,
3132                    expected.violated_constraint_relations,
3133                    self.trace.accepted_world_views,
3134                    expected.accepted_world_views
3135                ),
3136            });
3137        }
3138        if !self.trace.aggregate_kernel_timing.is_recorded() {
3139            return Err(XlogError::UnsupportedEpistemicConstruct {
3140                construct: construct.to_string(),
3141                context: "batch GPU execution did not record aggregate CUDA-event timing"
3142                    .to_string(),
3143            });
3144        }
3145        Ok(())
3146    }
3147}
3148
3149impl EpistemicGpuRuntimeWcojCertification {
3150    /// Compare static preflight obligations with runtime counter deltas.
3151    pub fn for_preflight_and_delta(
3152        preflight: &EpistemicGpuRuntimePreflight,
3153        delta: &EpistemicGpuRuntimeCounters,
3154    ) -> Self {
3155        Self::try_for_preflight_and_delta(preflight, delta)
3156            .expect("runtime WCOJ certification counters must not overflow")
3157    }
3158
3159    /// Compare static preflight obligations with runtime counter deltas, failing closed
3160    /// if certification counters overflow while being summarized.
3161    pub fn try_for_preflight_and_delta(
3162        preflight: &EpistemicGpuRuntimePreflight,
3163        delta: &EpistemicGpuRuntimeCounters,
3164    ) -> Result<Self> {
3165        let observed_wcoj_dispatches = delta.checked_wcoj_dispatch_count()?;
3166        let observed_kclique_dispatches = delta.checked_wcoj_clique_dispatch_count()?;
3167        let wcoj_routed_reduction_count = preflight
3168            .multiway_reduction_count
3169            .checked_sub(preflight.planned_hash_route_count)
3170            // Generic Free Join routes are opportunistic by
3171            // contract (structural decline executes the embedded
3172            // binary fallback), so they never form part of the hard
3173            // dedicated-WCOJ dispatch obligation.
3174            .and_then(|count| count.checked_sub(preflight.free_join_route_count))
3175            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
3176                construct: "epistemic GPU WCOJ route certification".to_string(),
3177                context: format!(
3178                    "planned hash + free-join routes exceed observed route obligations: \
3179                     multiway_reductions={} planned_hash_routes={} free_join_routes={}",
3180                    preflight.multiway_reduction_count,
3181                    preflight.planned_hash_route_count,
3182                    preflight.free_join_route_count
3183                ),
3184            })?;
3185        let required_multiway_reductions = wcoj_routed_reduction_count;
3186
3187        if required_multiway_reductions == 0 {
3188            return Ok(Self::NotRequired {
3189                observed_wcoj_dispatches,
3190                planned_hash_routes: preflight.planned_hash_route_count,
3191                planned_hash_planner_wins: preflight.planned_hash_planner_wins_count,
3192                planned_hash_incomplete_stats: preflight.planned_hash_incomplete_stats_count,
3193                planned_hash_cost_evidence: preflight.planned_hash_cost_evidence_count,
3194            });
3195        }
3196
3197        if observed_wcoj_dispatches < required_multiway_reductions as u64
3198            || observed_kclique_dispatches < preflight.kclique_wcoj_plan_count as u64
3199            || delta.wcoj_triangle_dispatch_count < preflight.wcoj_triangle_route_count as u64
3200            || delta.wcoj_4cycle_dispatch_count < preflight.wcoj_4cycle_route_count as u64
3201            || delta.wcoj_clique5_dispatch_count
3202                < preflight.kclique_wcoj_plan_count_by_arity[0] as u64
3203            || delta.wcoj_clique6_dispatch_count
3204                < preflight.kclique_wcoj_plan_count_by_arity[1] as u64
3205            || delta.wcoj_clique7_dispatch_count
3206                < preflight.kclique_wcoj_plan_count_by_arity[2] as u64
3207            || delta.wcoj_clique8_dispatch_count
3208                < preflight.kclique_wcoj_plan_count_by_arity[3] as u64
3209        {
3210            return Ok(Self::MissingRequiredWcojDispatch {
3211                required_multiway_reductions,
3212                required_kclique_plans: preflight.kclique_wcoj_plan_count,
3213                observed_wcoj_dispatches,
3214                observed_kclique_dispatches,
3215            });
3216        }
3217
3218        let observed_layout_events = EpistemicGpuRuntimeCounters::checked_counter_sum(
3219            "wcoj_layout_events",
3220            &[
3221                delta.wcoj_layout_sort_invocation_count,
3222                delta.wcoj_layout_fast_path_hit_count,
3223            ],
3224        )?;
3225        if observed_layout_events < preflight.sorted_layout_requirement_count as u64 {
3226            return Ok(Self::MissingRequiredWcojLayout {
3227                required_sorted_layouts: preflight.sorted_layout_requirement_count,
3228                observed_layout_events,
3229            });
3230        }
3231
3232        if preflight.kclique_wcoj_plan_count > 0
3233            && (delta.kclique_metadata_build_count < preflight.kclique_wcoj_plan_count as u64
3234                || delta.kclique_metadata_build_nanos == 0)
3235        {
3236            return Ok(Self::MissingRequiredKcliqueMetadata {
3237                required_kclique_plans: preflight.kclique_wcoj_plan_count,
3238                observed_metadata_builds: delta.kclique_metadata_build_count,
3239                observed_metadata_build_nanos: delta.kclique_metadata_build_nanos,
3240            });
3241        }
3242
3243        Ok(Self::Certified {
3244            observed_wcoj_dispatches,
3245            certified_multiway_reductions: required_multiway_reductions,
3246            observed_kclique_dispatches,
3247            certified_edge_permutation_slots: preflight.kclique_wcoj_edge_permutation_count,
3248            certified_stream_groups: preflight.kclique_stream_group_count,
3249            certified_skew_scheduled_plans: preflight.kclique_skew_scheduled_plan_count,
3250            certified_sorted_layout_requirements: preflight.sorted_layout_requirement_count,
3251            certified_helper_split_specs: preflight.helper_split_spec_count,
3252            certified_helper_relation_rules: preflight.helper_relation_rule_count,
3253            certified_helper_relation_scans: preflight.helper_relation_scan_count,
3254            observed_layout_sorts: delta.wcoj_layout_sort_invocation_count,
3255            observed_layout_fast_path_hits: delta.wcoj_layout_fast_path_hit_count,
3256            observed_metadata_builds: delta.kclique_metadata_build_count,
3257            observed_metadata_build_nanos: delta.kclique_metadata_build_nanos,
3258            observed_histogram_refreshes: delta.kclique_histogram_refresh_count,
3259            observed_histogram_refresh_nanos: delta.kclique_histogram_refresh_nanos,
3260        })
3261    }
3262}
3263
3264#[allow(clippy::large_enum_variant)]
3265enum TupleSourceLaunch<'a> {
3266    ArityZero {
3267        literal_index: u32,
3268        reduction_index: u32,
3269        negated: u8,
3270        row_count: &'a TrackedCudaSlice<u32>,
3271    },
3272    ArityOne {
3273        literal_index: u32,
3274        reduction_index: u32,
3275        negated: u8,
3276        row_count: &'a TrackedCudaSlice<u32>,
3277        key_col0: &'a CudaColumn,
3278        key_col0_width: u32,
3279        expected_key_col0_bits: u64,
3280        expected_key_col0_type_code: u8,
3281    },
3282    ArityTwo {
3283        literal_index: u32,
3284        reduction_index: u32,
3285        negated: u8,
3286        row_count: &'a TrackedCudaSlice<u32>,
3287        key_col0: &'a CudaColumn,
3288        key_col0_width: u32,
3289        expected_key_col0_bits: u64,
3290        expected_key_col0_type_code: u8,
3291        key_col1: &'a CudaColumn,
3292        key_col1_width: u32,
3293        expected_key_col1_bits: u64,
3294        expected_key_col1_type_code: u8,
3295    },
3296    ArityThree {
3297        literal_index: u32,
3298        reduction_index: u32,
3299        negated: u8,
3300        row_count: &'a TrackedCudaSlice<u32>,
3301        key_col0: &'a CudaColumn,
3302        key_col0_width: u32,
3303        expected_key_col0_bits: u64,
3304        expected_key_col0_type_code: u8,
3305        key_col1: &'a CudaColumn,
3306        key_col1_width: u32,
3307        expected_key_col1_bits: u64,
3308        expected_key_col1_type_code: u8,
3309        key_col2: &'a CudaColumn,
3310        key_col2_width: u32,
3311        expected_key_col2_bits: u64,
3312        expected_key_col2_type_code: u8,
3313    },
3314    ArityN {
3315        literal_index: u32,
3316        reduction_index: u32,
3317        negated: u8,
3318        row_count: &'a TrackedCudaSlice<u32>,
3319        bound_value_row_count: &'a TrackedCudaSlice<u32>,
3320        key_col_count: u32,
3321        key_col_ptrs: TrackedCudaSlice<u64>,
3322        key_col_widths: TrackedCudaSlice<u32>,
3323        expected_key_bits: TrackedCudaSlice<u64>,
3324        expected_key_type_codes: TrackedCudaSlice<u8>,
3325        tuple_key_match_modes: TrackedCudaSlice<u8>,
3326        bound_value_col_ptrs: TrackedCudaSlice<u64>,
3327        bound_value_col_widths: TrackedCudaSlice<u32>,
3328        has_bound_value_keys: u8,
3329    },
3330}
3331
3332const TUPLE_KEY_MATCH_MODE_GROUND: u8 = 0;
3333const TUPLE_KEY_MATCH_MODE_BOUND_OUTPUT: u8 = 1;
3334/// Anonymous wildcard tuple-key position: matches any stable-model value.
3335const TUPLE_KEY_MATCH_MODE_WILDCARD: u8 = 2;
3336
3337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3338struct TupleKeyExpectation {
3339    bits: u64,
3340    type_code: u8,
3341}
3342
3343impl TupleKeyExpectation {
3344    fn from_term(term: &EirTerm, column_type: ScalarType) -> Result<Self> {
3345        let bits = match (term, column_type) {
3346            (EirTerm::Integer(value), ScalarType::U32) => {
3347                u32::try_from(*value).map(u64::from).map_err(|_| {
3348                    tuple_key_expectation_error(format!(
3349                        "integer {value} is out of range for U32 tuple-key column"
3350                    ))
3351                })?
3352            }
3353            (EirTerm::Integer(value), ScalarType::I32) => i32::try_from(*value)
3354                .map(|v| v as u32 as u64)
3355                .map_err(|_| {
3356                    tuple_key_expectation_error(format!(
3357                        "integer {value} is out of range for I32 tuple-key column"
3358                    ))
3359                })?,
3360            (EirTerm::Integer(value), ScalarType::U64) => u64::try_from(*value).map_err(|_| {
3361                tuple_key_expectation_error(format!(
3362                    "integer {value} is out of range for U64 tuple-key column"
3363                ))
3364            })?,
3365            (EirTerm::Integer(value), ScalarType::I64) => *value as u64,
3366            (EirTerm::Integer(value), ScalarType::Bool) => match *value {
3367                0 => 0,
3368                1 => 1,
3369                _ => {
3370                    return Err(tuple_key_expectation_error(format!(
3371                        "integer {value} is out of range for Bool tuple-key column"
3372                    )))
3373                }
3374            },
3375            (EirTerm::Symbol(value), ScalarType::Symbol) => u64::from(*value),
3376            (EirTerm::String(value), ScalarType::Symbol) => {
3377                u64::from(xlog_core::symbol::intern(value))
3378            }
3379            (EirTerm::FloatBits(bits), ScalarType::F64) => *bits,
3380            (EirTerm::FloatBits(bits), ScalarType::F32) => {
3381                (f64::from_bits(*bits) as f32).to_bits() as u64
3382            }
3383            (EirTerm::Variable(_), _) => {
3384                return Err(tuple_key_expectation_error(format!(
3385                    "term {term:?} cannot be encoded as a ground tuple-key expectation"
3386                )))
3387            }
3388            (
3389                EirTerm::Anonymous
3390                | EirTerm::List(_)
3391                | EirTerm::Cons { .. }
3392                | EirTerm::Compound { .. }
3393                | EirTerm::PredRef(_)
3394                | EirTerm::Aggregate { .. },
3395                _,
3396            ) => {
3397                return Err(tuple_key_expectation_error(format!(
3398                    "term {term:?} cannot be used for GPU tuple-key matching"
3399                )))
3400            }
3401            _ => {
3402                return Err(tuple_key_expectation_error(format!(
3403                    "term {term:?} cannot be encoded for {column_type:?} tuple-key column"
3404                )))
3405            }
3406        };
3407
3408        Ok(Self {
3409            bits,
3410            type_code: column_type.to_code(),
3411        })
3412    }
3413}
3414
3415fn tuple_key_expectation_error(context: String) -> XlogError {
3416    XlogError::UnsupportedEpistemicConstruct {
3417        construct: "epistemic GPU tuple-key expectation".to_string(),
3418        context,
3419    }
3420}
3421
3422impl Executor {
3423    /// Resolve a modal tuple-source relation, disambiguating same-name multi-arity
3424    /// modal predicates by arity.
3425    ///
3426    /// The relation store is keyed by name, so a program using the SAME predicate
3427    /// name at two different arities in modal literals (`know p(X)` over `p/1` AND
3428    /// `possible p(X,Y)` over `p/2`) could not resolve both sources under the bare
3429    /// name. Distinct arities ARE distinct relations, so this resolves the
3430    /// ARITY-QUALIFIED store key (`"p/1"`, `"p/2"`) FIRST, falling back to the bare
3431    /// predicate name when no qualified entry exists. Single-arity epistemic
3432    /// programs keep uploading under the bare name and hit the fallback unchanged
3433    /// (no regression); a multi-arity program uploads each arity under its own
3434    /// qualified key and both resolve distinctly.
3435    ///
3436    /// The `"/"` separator is collision-safe: parser predicate names cannot contain
3437    /// `"/"`, so a qualified key can never shadow a real bare-name relation.
3438    ///
3439    /// Resolution is structural (driven by `arity`, never by a specific arity VALUE
3440    /// or predicate NAME), so it introduces no special-casing.
3441    fn resolve_modal_tuple_source(&self, predicate: &str, arity: usize) -> Option<&CudaBuffer> {
3442        let qualified = format!("{predicate}/{arity}");
3443        self.store()
3444            .get(qualified.as_str())
3445            .or_else(|| self.store().get(predicate))
3446    }
3447
3448    /// Snapshot runtime counters used by epistemic GPU certification.
3449    pub fn epistemic_gpu_runtime_counters(&self) -> EpistemicGpuRuntimeCounters {
3450        EpistemicGpuRuntimeCounters {
3451            wcoj_triangle_dispatch_count: self.wcoj_triangle_dispatch_count(),
3452            wcoj_4cycle_dispatch_count: self.wcoj_4cycle_dispatch_count(),
3453            chain_dispatch_count: self.chain_dispatch_count(),
3454            wcoj_clique5_dispatch_count: self.wcoj_clique5_dispatch_count(),
3455            wcoj_clique6_dispatch_count: self.wcoj_clique6_dispatch_count(),
3456            wcoj_clique7_dispatch_count: self.wcoj_clique7_dispatch_count(),
3457            wcoj_clique8_dispatch_count: self.wcoj_clique8_dispatch_count(),
3458            free_join_dispatch_count: self.free_join_dispatch_count(),
3459            factorized_delta_dispatch_count: self.factorized_delta_dispatch_count(),
3460            provider_wcoj_triangle_hg_dispatch_count: self
3461                .provider
3462                .wcoj_triangle_hg_dispatch_count(),
3463            wcoj_layout_sort_invocation_count: self.provider.wcoj_layout_sort_invocation_count(),
3464            wcoj_layout_fast_path_hit_count: self.provider.wcoj_layout_fast_path_hit_count(),
3465            kclique_metadata_build_count: self.provider.kclique_metadata_build_count(),
3466            kclique_metadata_build_nanos: self.provider.kclique_metadata_build_nanos(),
3467            kclique_histogram_refresh_count: self.kclique_histogram_refresh_count(),
3468            kclique_histogram_refresh_nanos: self.kclique_histogram_refresh_nanos(),
3469        }
3470    }
3471
3472    fn time_epistemic_gpu_kernel_launch(
3473        &self,
3474        operation: &str,
3475        launch: impl FnOnce() -> std::result::Result<(), DriverError>,
3476    ) -> Result<EpistemicGpuKernelTimingTrace> {
3477        let stream = self.provider.device().inner().stream().clone();
3478        let start = stream
3479            .record_event(Some(sys::CUevent_flags::CU_EVENT_DEFAULT))
3480            .map_err(|e| XlogError::execution_ctx(operation, "record start timing event", &e))?;
3481        launch().map_err(|e| XlogError::execution_ctx(operation, "launch kernel", &e))?;
3482        let end = stream
3483            .record_event(Some(sys::CUevent_flags::CU_EVENT_DEFAULT))
3484            .map_err(|e| XlogError::execution_ctx(operation, "record end timing event", &e))?;
3485        let elapsed_ms = start
3486            .elapsed_ms(&end)
3487            .map_err(|e| XlogError::execution_ctx(operation, "measure CUDA event elapsed", &e))?;
3488
3489        EpistemicGpuKernelTimingTrace::from_cuda_elapsed_ms(elapsed_ms)
3490    }
3491
3492    /// Allocate GPU-resident buffers required by an epistemic GPU plan.
3493    pub fn allocate_epistemic_gpu_workspace(
3494        &self,
3495        plan: &EpistemicGpuPlan,
3496        capacities: EpistemicGpuWorkspaceCapacities,
3497    ) -> Result<EpistemicGpuWorkspace> {
3498        let layout = EpistemicGpuWorkspaceLayout::for_plan(plan, capacities)?;
3499        let memory = self.provider.memory();
3500
3501        Ok(EpistemicGpuWorkspace {
3502            layout,
3503            candidate_assumptions: memory.alloc::<u8>(layout.candidate_assumption_bytes)?,
3504            world_views: memory.alloc::<u8>(layout.world_view_bytes)?,
3505            model_membership: memory.alloc::<u8>(layout.model_membership_bytes)?,
3506            rejection_reasons: memory.alloc::<u32>(layout.rejection_reason_slots)?,
3507            constraint_violation_index: memory.alloc::<u32>(layout.rejection_reason_slots)?,
3508        })
3509    }
3510
3511    /// Zero every epistemic workspace buffer on device before hot-path use.
3512    pub fn reset_epistemic_gpu_workspace(
3513        &self,
3514        workspace: &mut EpistemicGpuWorkspace,
3515    ) -> Result<EpistemicGpuWorkspaceResetTrace> {
3516        let device = self.provider.device().inner();
3517
3518        device
3519            .memset_zeros(&mut workspace.candidate_assumptions)
3520            .map_err(|e| {
3521                XlogError::execution_ctx(
3522                    "epistemic GPU workspace reset",
3523                    "candidate assumptions memset",
3524                    &e,
3525                )
3526            })?;
3527        device
3528            .memset_zeros(&mut workspace.world_views)
3529            .map_err(|e| {
3530                XlogError::execution_ctx("epistemic GPU workspace reset", "world views memset", &e)
3531            })?;
3532        device
3533            .memset_zeros(&mut workspace.model_membership)
3534            .map_err(|e| {
3535                XlogError::execution_ctx(
3536                    "epistemic GPU workspace reset",
3537                    "model membership memset",
3538                    &e,
3539                )
3540            })?;
3541        device
3542            .memset_zeros(&mut workspace.rejection_reasons)
3543            .map_err(|e| {
3544                XlogError::execution_ctx(
3545                    "epistemic GPU workspace reset",
3546                    "rejection reasons memset",
3547                    &e,
3548                )
3549            })?;
3550
3551        EpistemicGpuWorkspaceResetTrace::try_for_layout(workspace.layout)
3552    }
3553
3554    /// Generate candidate-assumption bitsets directly into the GPU workspace.
3555    pub fn generate_epistemic_gpu_candidates(
3556        &self,
3557        workspace: &mut EpistemicGpuWorkspace,
3558        literal_count: usize,
3559        candidate_count: usize,
3560    ) -> Result<EpistemicGpuCandidateGenerationTrace> {
3561        let trace =
3562            EpistemicGpuCandidateGenerationTrace::for_counts(literal_count, candidate_count)?;
3563        if trace.candidate_assumption_bytes > workspace.layout.candidate_assumption_bytes {
3564            return Err(XlogError::ResourceExhausted {
3565                context: "epistemic GPU candidate assumption workspace".to_string(),
3566                estimated_bytes: trace.candidate_assumption_bytes as u64,
3567                budget_bytes: workspace.layout.candidate_assumption_bytes as u64,
3568            });
3569        }
3570        if trace.candidate_assumption_bytes > u32::MAX as usize {
3571            return Err(XlogError::ResourceExhausted {
3572                context: "epistemic GPU candidate generation launch".to_string(),
3573                estimated_bytes: trace.candidate_assumption_bytes as u64,
3574                budget_bytes: u32::MAX as u64,
3575            });
3576        }
3577
3578        let literal_count =
3579            checked_u32_dimension(literal_count, "epistemic GPU candidate generation literals")?;
3580        let candidate_count = checked_u32_dimension(
3581            candidate_count,
3582            "epistemic GPU candidate generation candidates",
3583        )?;
3584        let total = checked_u32_dimension(
3585            trace.candidate_assumption_bytes,
3586            "epistemic GPU candidate generation launch elements",
3587        )?;
3588        let func = self
3589            .provider
3590            .device()
3591            .inner()
3592            .get_func(
3593                EPISTEMIC_MODULE,
3594                epistemic_kernels::EPISTEMIC_GENERATE_CANDIDATE_ASSUMPTIONS_U8,
3595            )
3596            .ok_or_else(|| {
3597                XlogError::Execution("epistemic candidate generation kernel not found".to_string())
3598            })?;
3599        let config = LaunchConfig::for_num_elems(total);
3600
3601        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
3602            "epistemic GPU candidate generation",
3603            || unsafe {
3604                // SAFETY: kernel arguments match the PTX signature; the workspace capacity check
3605                // above proves the output buffer covers literal_count * candidate_count bytes.
3606                func.clone().launch(
3607                    config,
3608                    (
3609                        literal_count,
3610                        candidate_count,
3611                        &mut workspace.candidate_assumptions,
3612                    ),
3613                )
3614            },
3615        )?;
3616
3617        Ok(trace.with_kernel_timing(kernel_timing))
3618    }
3619
3620    /// Propagate generated candidates into GPU-resident world-view staging buffers.
3621    pub fn propagate_epistemic_gpu_candidates(
3622        &self,
3623        workspace: &mut EpistemicGpuWorkspace,
3624        literal_count: usize,
3625        candidate_count: usize,
3626    ) -> Result<EpistemicGpuPropagationTrace> {
3627        let mut trace = EpistemicGpuPropagationTrace::for_counts(literal_count, candidate_count)?;
3628        let candidate_assumption_bytes = checked_product(literal_count, candidate_count)?;
3629        if candidate_assumption_bytes > workspace.layout.candidate_assumption_bytes {
3630            return Err(XlogError::ResourceExhausted {
3631                context: "epistemic GPU propagation candidate workspace".to_string(),
3632                estimated_bytes: candidate_assumption_bytes as u64,
3633                budget_bytes: workspace.layout.candidate_assumption_bytes as u64,
3634            });
3635        }
3636        if trace.rejection_reason_slots_written > workspace.layout.rejection_reason_slots {
3637            return Err(XlogError::ResourceExhausted {
3638                context: "epistemic GPU propagation rejection workspace".to_string(),
3639                estimated_bytes: trace.rejection_reason_slots_written as u64,
3640                budget_bytes: workspace.layout.rejection_reason_slots as u64,
3641            });
3642        }
3643        if literal_count > u32::MAX as usize || candidate_count > u32::MAX as usize {
3644            return Err(XlogError::ResourceExhausted {
3645                context: "epistemic GPU propagation launch".to_string(),
3646                estimated_bytes: literal_count.max(candidate_count) as u64,
3647                budget_bytes: u32::MAX as u64,
3648            });
3649        }
3650
3651        let world_stride =
3652            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
3653        if world_stride == 0 || world_stride > u32::MAX as usize {
3654            return Err(XlogError::ResourceExhausted {
3655                context: "epistemic GPU propagation world stride".to_string(),
3656                estimated_bytes: world_stride as u64,
3657                budget_bytes: u32::MAX as u64,
3658            });
3659        }
3660        let world_view_bitset_bytes_per_candidate =
3661            world_view_bitset_bytes_per_candidate(literal_count)?;
3662        if world_view_bitset_bytes_per_candidate > world_stride {
3663            return Err(XlogError::ResourceExhausted {
3664                context: "epistemic GPU propagation world-view bitset stride".to_string(),
3665                estimated_bytes: world_view_bitset_bytes_per_candidate as u64,
3666                budget_bytes: world_stride as u64,
3667            });
3668        }
3669        let world_view_bitset_bytes =
3670            checked_product(world_view_bitset_bytes_per_candidate, candidate_count)?;
3671        if world_view_bitset_bytes > workspace.layout.world_view_bytes {
3672            return Err(XlogError::ResourceExhausted {
3673                context: "epistemic GPU propagation world-view bitsets".to_string(),
3674                estimated_bytes: world_view_bitset_bytes as u64,
3675                budget_bytes: workspace.layout.world_view_bytes as u64,
3676            });
3677        }
3678        trace.world_view_bytes_written = checked_product(world_stride, candidate_count)?;
3679        if trace.world_view_bytes_written > workspace.layout.world_view_bytes {
3680            return Err(XlogError::ResourceExhausted {
3681                context: "epistemic GPU propagation world-view workspace".to_string(),
3682                estimated_bytes: trace.world_view_bytes_written as u64,
3683                budget_bytes: workspace.layout.world_view_bytes as u64,
3684            });
3685        }
3686
3687        let literal_count =
3688            checked_u32_dimension(literal_count, "epistemic GPU propagation literals")?;
3689        let candidate_count =
3690            checked_u32_dimension(candidate_count, "epistemic GPU propagation candidates")?;
3691        let world_stride =
3692            checked_u32_dimension(world_stride, "epistemic GPU propagation world stride")?;
3693        let func = self
3694            .provider
3695            .device()
3696            .inner()
3697            .get_func(
3698                EPISTEMIC_MODULE,
3699                epistemic_kernels::EPISTEMIC_PROPAGATE_CANDIDATES_U8,
3700            )
3701            .ok_or_else(|| {
3702                XlogError::Execution("epistemic candidate propagation kernel not found".to_string())
3703            })?;
3704        let config = LaunchConfig::for_num_elems(candidate_count);
3705
3706        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
3707            "epistemic GPU candidate propagation",
3708            || unsafe {
3709                // SAFETY: kernel arguments match the PTX signature; the capacity checks
3710                // above prove candidate, world-view, and rejection buffers cover all writes.
3711                func.clone().launch(
3712                    config,
3713                    (
3714                        literal_count,
3715                        candidate_count,
3716                        world_stride,
3717                        &workspace.candidate_assumptions,
3718                        &mut workspace.world_views,
3719                        &mut workspace.rejection_reasons,
3720                    ),
3721                )
3722            },
3723        )?;
3724
3725        Ok(trace.with_kernel_timing(kernel_timing))
3726    }
3727
3728    /// Validate staged candidate bitsets and world-view activity on device.
3729    pub fn validate_epistemic_gpu_candidates(
3730        &self,
3731        workspace: &mut EpistemicGpuWorkspace,
3732        literal_count: usize,
3733        candidate_count: usize,
3734    ) -> Result<EpistemicGpuCandidateValidationTrace> {
3735        let mut trace =
3736            EpistemicGpuCandidateValidationTrace::for_counts(literal_count, candidate_count)?;
3737        if trace.candidate_assumption_bytes_checked > workspace.layout.candidate_assumption_bytes {
3738            return Err(XlogError::ResourceExhausted {
3739                context: "epistemic GPU validation candidate workspace".to_string(),
3740                estimated_bytes: trace.candidate_assumption_bytes_checked as u64,
3741                budget_bytes: workspace.layout.candidate_assumption_bytes as u64,
3742            });
3743        }
3744        if trace.rejection_reason_slots_written > workspace.layout.rejection_reason_slots {
3745            return Err(XlogError::ResourceExhausted {
3746                context: "epistemic GPU validation rejection workspace".to_string(),
3747                estimated_bytes: trace.rejection_reason_slots_written as u64,
3748                budget_bytes: workspace.layout.rejection_reason_slots as u64,
3749            });
3750        }
3751        if literal_count > u32::MAX as usize || candidate_count > u32::MAX as usize {
3752            return Err(XlogError::ResourceExhausted {
3753                context: "epistemic GPU validation launch".to_string(),
3754                estimated_bytes: literal_count.max(candidate_count) as u64,
3755                budget_bytes: u32::MAX as u64,
3756            });
3757        }
3758
3759        let world_stride =
3760            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
3761        if world_stride == 0 || world_stride > u32::MAX as usize {
3762            return Err(XlogError::ResourceExhausted {
3763                context: "epistemic GPU validation world stride".to_string(),
3764                estimated_bytes: world_stride as u64,
3765                budget_bytes: u32::MAX as u64,
3766            });
3767        }
3768        let world_view_bitset_bytes_per_candidate =
3769            world_view_bitset_bytes_per_candidate(literal_count)?;
3770        if world_view_bitset_bytes_per_candidate > world_stride {
3771            return Err(XlogError::ResourceExhausted {
3772                context: "epistemic GPU validation world-view bitset stride".to_string(),
3773                estimated_bytes: world_view_bitset_bytes_per_candidate as u64,
3774                budget_bytes: world_stride as u64,
3775            });
3776        }
3777        let world_view_bitset_bytes =
3778            checked_product(world_view_bitset_bytes_per_candidate, candidate_count)?;
3779        if world_view_bitset_bytes > workspace.layout.world_view_bytes {
3780            return Err(XlogError::ResourceExhausted {
3781                context: "epistemic GPU validation world-view bitsets".to_string(),
3782                estimated_bytes: world_view_bitset_bytes as u64,
3783                budget_bytes: workspace.layout.world_view_bytes as u64,
3784            });
3785        }
3786        trace.world_view_bytes_checked = world_view_bitset_bytes;
3787        if trace.world_view_bytes_checked > workspace.layout.world_view_bytes {
3788            return Err(XlogError::ResourceExhausted {
3789                context: "epistemic GPU validation world-view workspace".to_string(),
3790                estimated_bytes: trace.world_view_bytes_checked as u64,
3791                budget_bytes: workspace.layout.world_view_bytes as u64,
3792            });
3793        }
3794
3795        let literal_count =
3796            checked_u32_dimension(literal_count, "epistemic GPU validation literals")?;
3797        let candidate_count =
3798            checked_u32_dimension(candidate_count, "epistemic GPU validation candidates")?;
3799        let world_stride =
3800            checked_u32_dimension(world_stride, "epistemic GPU validation world stride")?;
3801        let func = self
3802            .provider
3803            .device()
3804            .inner()
3805            .get_func(
3806                EPISTEMIC_MODULE,
3807                epistemic_kernels::EPISTEMIC_VALIDATE_CANDIDATE_BITS_U8,
3808            )
3809            .ok_or_else(|| {
3810                XlogError::Execution("epistemic candidate validation kernel not found".to_string())
3811            })?;
3812        let config = LaunchConfig::for_num_elems(candidate_count);
3813
3814        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
3815            "epistemic GPU candidate validation",
3816            || unsafe {
3817                // SAFETY: kernel arguments match the PTX signature; the capacity checks
3818                // above prove candidate, world-view, and rejection buffers cover all accesses.
3819                func.clone().launch(
3820                    config,
3821                    (
3822                        literal_count,
3823                        candidate_count,
3824                        world_stride,
3825                        &workspace.candidate_assumptions,
3826                        &workspace.world_views,
3827                        &mut workspace.rejection_reasons,
3828                    ),
3829                )
3830            },
3831        )?;
3832
3833        Ok(trace.with_kernel_timing(kernel_timing))
3834    }
3835
3836    /// Populate candidate-scoped model-membership staging buffers on device.
3837    pub fn populate_epistemic_gpu_model_membership(
3838        &self,
3839        workspace: &mut EpistemicGpuWorkspace,
3840        output: &CudaBuffer,
3841        literal_count: usize,
3842        candidate_count: usize,
3843        reduction_count: usize,
3844        models_per_reduction: usize,
3845    ) -> Result<EpistemicGpuModelMembershipTrace> {
3846        let trace = EpistemicGpuModelMembershipTrace::for_counts(
3847            literal_count,
3848            candidate_count,
3849            reduction_count,
3850            models_per_reduction,
3851        )?;
3852        let candidate_assumption_bytes = checked_product(literal_count, candidate_count)?;
3853        if candidate_assumption_bytes > workspace.layout.candidate_assumption_bytes {
3854            return Err(XlogError::ResourceExhausted {
3855                context: "epistemic GPU model-membership candidate workspace".to_string(),
3856                estimated_bytes: candidate_assumption_bytes as u64,
3857                budget_bytes: workspace.layout.candidate_assumption_bytes as u64,
3858            });
3859        }
3860        if candidate_count > workspace.layout.world_view_bytes {
3861            return Err(XlogError::ResourceExhausted {
3862                context: "epistemic GPU model-membership world-view workspace".to_string(),
3863                estimated_bytes: candidate_count as u64,
3864                budget_bytes: workspace.layout.world_view_bytes as u64,
3865            });
3866        }
3867        if trace.model_membership_bytes_written > workspace.layout.model_membership_bytes {
3868            return Err(XlogError::ResourceExhausted {
3869                context: "epistemic GPU model-membership workspace".to_string(),
3870                estimated_bytes: trace.model_membership_bytes_written as u64,
3871                budget_bytes: workspace.layout.model_membership_bytes as u64,
3872            });
3873        }
3874        if trace.rejection_reason_slots_checked > workspace.layout.rejection_reason_slots {
3875            return Err(XlogError::ResourceExhausted {
3876                context: "epistemic GPU model-membership rejection workspace".to_string(),
3877                estimated_bytes: trace.rejection_reason_slots_checked as u64,
3878                budget_bytes: workspace.layout.rejection_reason_slots as u64,
3879            });
3880        }
3881        if trace.model_membership_bytes_written > u32::MAX as usize {
3882            return Err(XlogError::ResourceExhausted {
3883                context: "epistemic GPU model-membership launch".to_string(),
3884                estimated_bytes: trace.model_membership_bytes_written as u64,
3885                budget_bytes: u32::MAX as u64,
3886            });
3887        }
3888        if literal_count > u32::MAX as usize
3889            || candidate_count > u32::MAX as usize
3890            || reduction_count > u32::MAX as usize
3891            || models_per_reduction > u32::MAX as usize
3892        {
3893            return Err(XlogError::ResourceExhausted {
3894                context: "epistemic GPU model-membership dimensions".to_string(),
3895                estimated_bytes: literal_count
3896                    .max(candidate_count)
3897                    .max(reduction_count)
3898                    .max(models_per_reduction) as u64,
3899                budget_bytes: u32::MAX as u64,
3900            });
3901        }
3902
3903        let world_stride =
3904            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
3905        if world_stride == 0 || world_stride > u32::MAX as usize {
3906            return Err(XlogError::ResourceExhausted {
3907                context: "epistemic GPU model-membership world stride".to_string(),
3908                estimated_bytes: world_stride as u64,
3909                budget_bytes: u32::MAX as u64,
3910            });
3911        }
3912
3913        let literal_count = literal_count as u32;
3914        let candidate_count = candidate_count as u32;
3915        let reduction_count = reduction_count as u32;
3916        let models_per_reduction = models_per_reduction as u32;
3917        let world_stride = world_stride as u32;
3918        let total = trace.model_membership_bytes_written as u32;
3919        let func = self
3920            .provider
3921            .device()
3922            .inner()
3923            .get_func(
3924                EPISTEMIC_MODULE,
3925                epistemic_kernels::EPISTEMIC_POPULATE_MODEL_MEMBERSHIP_U8,
3926            )
3927            .ok_or_else(|| {
3928                XlogError::Execution("epistemic model-membership kernel not found".to_string())
3929            })?;
3930        let config = LaunchConfig::for_num_elems(total);
3931
3932        let kernel_timing =
3933            self.time_epistemic_gpu_kernel_launch("epistemic GPU model membership", || unsafe {
3934                // SAFETY: kernel arguments match the PTX signature; the capacity checks
3935                // above prove candidate, world-view, membership, and rejection buffers
3936                // cover all reads and writes.
3937                func.clone().launch(
3938                    config,
3939                    (
3940                        literal_count,
3941                        candidate_count,
3942                        reduction_count,
3943                        models_per_reduction,
3944                        world_stride,
3945                        output.num_rows_device(),
3946                        &workspace.candidate_assumptions,
3947                        &workspace.world_views,
3948                        &mut workspace.model_membership,
3949                        &mut workspace.rejection_reasons,
3950                    ),
3951                )
3952            })?;
3953
3954        Ok(trace.with_kernel_timing(kernel_timing))
3955    }
3956
3957    /// Populate model-membership bytes from reduced stable-model tuple sources.
3958    pub fn populate_epistemic_gpu_model_membership_from_tuple_sources(
3959        &self,
3960        workspace: &mut EpistemicGpuWorkspace,
3961        output: &CudaBuffer,
3962        gpu_plan: &EpistemicGpuPlan,
3963        candidate_count: usize,
3964        models_per_reduction: usize,
3965    ) -> Result<EpistemicGpuModelMembershipTrace> {
3966        gpu_plan.validate_tuple_membership_bindings()?;
3967
3968        let literal_count = gpu_plan.epistemic_literals.len();
3969        let reduction_count = gpu_plan.reductions.len();
3970        let tuple_source_key_column_count = gpu_plan
3971            .tuple_membership_bindings
3972            .iter()
3973            .try_fold(0usize, |acc, binding| {
3974                checked_sum(acc, binding.key_columns.len())
3975            })?;
3976        let mut trace =
3977            EpistemicGpuModelMembershipTrace::for_stable_model_tuple_sources_with_key_columns(
3978                literal_count,
3979                candidate_count,
3980                reduction_count,
3981                models_per_reduction,
3982                gpu_plan.tuple_membership_bindings.len(),
3983                tuple_source_key_column_count,
3984            )?;
3985        trace.output_row_count_device_reads = trace.kernel_launches;
3986        let candidate_assumption_bytes = checked_product(literal_count, candidate_count)?;
3987        if candidate_assumption_bytes > workspace.layout.candidate_assumption_bytes {
3988            return Err(XlogError::ResourceExhausted {
3989                context: "epistemic GPU model-membership candidate workspace".to_string(),
3990                estimated_bytes: candidate_assumption_bytes as u64,
3991                budget_bytes: workspace.layout.candidate_assumption_bytes as u64,
3992            });
3993        }
3994        if candidate_count > workspace.layout.world_view_bytes {
3995            return Err(XlogError::ResourceExhausted {
3996                context: "epistemic GPU model-membership world-view workspace".to_string(),
3997                estimated_bytes: candidate_count as u64,
3998                budget_bytes: workspace.layout.world_view_bytes as u64,
3999            });
4000        }
4001        if trace.model_membership_bytes_written > workspace.layout.model_membership_bytes {
4002            return Err(XlogError::ResourceExhausted {
4003                context: "epistemic GPU model-membership workspace".to_string(),
4004                estimated_bytes: trace.model_membership_bytes_written as u64,
4005                budget_bytes: workspace.layout.model_membership_bytes as u64,
4006            });
4007        }
4008        if trace.rejection_reason_slots_checked > workspace.layout.rejection_reason_slots {
4009            return Err(XlogError::ResourceExhausted {
4010                context: "epistemic GPU model-membership rejection workspace".to_string(),
4011                estimated_bytes: trace.rejection_reason_slots_checked as u64,
4012                budget_bytes: workspace.layout.rejection_reason_slots as u64,
4013            });
4014        }
4015        if trace.model_membership_bytes_written > u32::MAX as usize {
4016            return Err(XlogError::ResourceExhausted {
4017                context: "epistemic GPU model-membership launch".to_string(),
4018                estimated_bytes: trace.model_membership_bytes_written as u64,
4019                budget_bytes: u32::MAX as u64,
4020            });
4021        }
4022        if literal_count > u32::MAX as usize
4023            || candidate_count > u32::MAX as usize
4024            || reduction_count > u32::MAX as usize
4025            || models_per_reduction > u32::MAX as usize
4026        {
4027            return Err(XlogError::ResourceExhausted {
4028                context: "epistemic GPU model-membership dimensions".to_string(),
4029                estimated_bytes: literal_count
4030                    .max(candidate_count)
4031                    .max(reduction_count)
4032                    .max(models_per_reduction) as u64,
4033                budget_bytes: u32::MAX as u64,
4034            });
4035        }
4036
4037        let world_stride =
4038            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
4039        if world_stride == 0 || world_stride > u32::MAX as usize {
4040            return Err(XlogError::ResourceExhausted {
4041                context: "epistemic GPU model-membership world stride".to_string(),
4042                estimated_bytes: world_stride as u64,
4043                budget_bytes: u32::MAX as u64,
4044            });
4045        }
4046
4047        let per_binding_launch_elems = checked_product(candidate_count, models_per_reduction)?;
4048        if per_binding_launch_elems > u32::MAX as usize {
4049            return Err(XlogError::ResourceExhausted {
4050                context: "epistemic GPU model-membership tuple-source launch".to_string(),
4051                estimated_bytes: per_binding_launch_elems as u64,
4052                budget_bytes: u32::MAX as u64,
4053            });
4054        }
4055
4056        let mut tuple_sources = Vec::with_capacity(gpu_plan.tuple_membership_bindings.len());
4057        for binding in &gpu_plan.tuple_membership_bindings {
4058            let source_relation = self
4059                .resolve_modal_tuple_source(binding.predicate.as_str(), binding.arity)
4060                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4061                    construct: "epistemic GPU stable-model tuple membership".to_string(),
4062                    context: format!(
4063                        "missing reduced stable-model tuple source relation {} (arity {})",
4064                        binding.predicate, binding.arity
4065                    ),
4066                })?;
4067            if source_relation.arity() != binding.arity {
4068                return Err(XlogError::UnsupportedEpistemicConstruct {
4069                    construct: "epistemic GPU stable-model tuple membership".to_string(),
4070                    context: format!(
4071                        "tuple source relation {} arity {} does not match binding arity {}",
4072                        binding.predicate,
4073                        source_relation.arity(),
4074                        binding.arity
4075                    ),
4076                });
4077            }
4078            let has_bound_value_keys = binding
4079                .key_terms
4080                .iter()
4081                .any(|term| matches!(term, EirTerm::Variable(_)));
4082            // Anonymous wildcards are value-level matches handled only by the
4083            // general arm; route any binding carrying a variable or an anonymous
4084            // term there. The specialized arity arms remain a fast path for
4085            // all-ground tuple keys.
4086            let has_value_level_keys = binding
4087                .key_terms
4088                .iter()
4089                .any(|term| matches!(term, EirTerm::Variable(_) | EirTerm::Anonymous));
4090            match binding.key_columns.as_slice() {
4091                [] => tuple_sources.push(TupleSourceLaunch::ArityZero {
4092                    literal_index: binding.literal_index as u32,
4093                    reduction_index: binding.reduction_index as u32,
4094                    negated: binding.negated as u8,
4095                    row_count: source_relation.num_rows_device(),
4096                }),
4097                &[key_col] if !has_value_level_keys => {
4098                    let key_col0 = source_relation.column(key_col).ok_or_else(|| {
4099                        XlogError::UnsupportedEpistemicConstruct {
4100                            construct: "epistemic GPU stable-model tuple membership".to_string(),
4101                            context: format!(
4102                                "tuple source relation {} missing key column {}",
4103                                binding.predicate, key_col
4104                            ),
4105                        }
4106                    })?;
4107                    let key_col0_type =
4108                        source_relation
4109                            .schema()
4110                            .column_type(key_col)
4111                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4112                                construct: "epistemic GPU stable-model tuple membership"
4113                                    .to_string(),
4114                                context: format!(
4115                                    "tuple source relation {} missing schema for key column {}",
4116                                    binding.predicate, key_col
4117                                ),
4118                            })?;
4119                    let key_col0_width = key_col0_type.size_bytes();
4120                    let key_col0_expectation =
4121                        TupleKeyExpectation::from_term(&binding.key_terms[0], key_col0_type)?;
4122                    if key_col0_width > u32::MAX as usize {
4123                        return Err(XlogError::ResourceExhausted {
4124                            context: "epistemic GPU tuple-key column width".to_string(),
4125                            estimated_bytes: key_col0_width as u64,
4126                            budget_bytes: u32::MAX as u64,
4127                        });
4128                    }
4129                    tuple_sources.push(TupleSourceLaunch::ArityOne {
4130                        literal_index: binding.literal_index as u32,
4131                        reduction_index: binding.reduction_index as u32,
4132                        negated: binding.negated as u8,
4133                        row_count: source_relation.num_rows_device(),
4134                        key_col0,
4135                        key_col0_width: key_col0_width as u32,
4136                        expected_key_col0_bits: key_col0_expectation.bits,
4137                        expected_key_col0_type_code: key_col0_expectation.type_code,
4138                    });
4139                }
4140                &[key_col0, key_col1] if !has_value_level_keys => {
4141                    let key_col0_ref = source_relation.column(key_col0).ok_or_else(|| {
4142                        XlogError::UnsupportedEpistemicConstruct {
4143                            construct: "epistemic GPU stable-model tuple membership".to_string(),
4144                            context: format!(
4145                                "tuple source relation {} missing key column {}",
4146                                binding.predicate, key_col0
4147                            ),
4148                        }
4149                    })?;
4150                    let key_col1_ref = source_relation.column(key_col1).ok_or_else(|| {
4151                        XlogError::UnsupportedEpistemicConstruct {
4152                            construct: "epistemic GPU stable-model tuple membership".to_string(),
4153                            context: format!(
4154                                "tuple source relation {} missing key column {}",
4155                                binding.predicate, key_col1
4156                            ),
4157                        }
4158                    })?;
4159                    let key_col0_type =
4160                        source_relation
4161                            .schema()
4162                            .column_type(key_col0)
4163                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4164                                construct: "epistemic GPU stable-model tuple membership"
4165                                    .to_string(),
4166                                context: format!(
4167                                    "tuple source relation {} missing schema for key column {}",
4168                                    binding.predicate, key_col0
4169                                ),
4170                            })?;
4171                    let key_col1_type =
4172                        source_relation
4173                            .schema()
4174                            .column_type(key_col1)
4175                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4176                                construct: "epistemic GPU stable-model tuple membership"
4177                                    .to_string(),
4178                                context: format!(
4179                                    "tuple source relation {} missing schema for key column {}",
4180                                    binding.predicate, key_col1
4181                                ),
4182                            })?;
4183                    let key_col0_width = key_col0_type.size_bytes();
4184                    let key_col1_width = key_col1_type.size_bytes();
4185                    let key_col0_expectation =
4186                        TupleKeyExpectation::from_term(&binding.key_terms[0], key_col0_type)?;
4187                    let key_col1_expectation =
4188                        TupleKeyExpectation::from_term(&binding.key_terms[1], key_col1_type)?;
4189                    let max_width = key_col0_width.max(key_col1_width);
4190                    if max_width > u32::MAX as usize {
4191                        return Err(XlogError::ResourceExhausted {
4192                            context: "epistemic GPU tuple-key column width".to_string(),
4193                            estimated_bytes: max_width as u64,
4194                            budget_bytes: u32::MAX as u64,
4195                        });
4196                    }
4197                    tuple_sources.push(TupleSourceLaunch::ArityTwo {
4198                        literal_index: binding.literal_index as u32,
4199                        reduction_index: binding.reduction_index as u32,
4200                        negated: binding.negated as u8,
4201                        row_count: source_relation.num_rows_device(),
4202                        key_col0: key_col0_ref,
4203                        key_col0_width: key_col0_width as u32,
4204                        expected_key_col0_bits: key_col0_expectation.bits,
4205                        expected_key_col0_type_code: key_col0_expectation.type_code,
4206                        key_col1: key_col1_ref,
4207                        key_col1_width: key_col1_width as u32,
4208                        expected_key_col1_bits: key_col1_expectation.bits,
4209                        expected_key_col1_type_code: key_col1_expectation.type_code,
4210                    });
4211                }
4212                &[key_col0, key_col1, key_col2] if !has_value_level_keys => {
4213                    let key_col0_ref = source_relation.column(key_col0).ok_or_else(|| {
4214                        XlogError::UnsupportedEpistemicConstruct {
4215                            construct: "epistemic GPU stable-model tuple membership".to_string(),
4216                            context: format!(
4217                                "tuple source relation {} missing key column {}",
4218                                binding.predicate, key_col0
4219                            ),
4220                        }
4221                    })?;
4222                    let key_col1_ref = source_relation.column(key_col1).ok_or_else(|| {
4223                        XlogError::UnsupportedEpistemicConstruct {
4224                            construct: "epistemic GPU stable-model tuple membership".to_string(),
4225                            context: format!(
4226                                "tuple source relation {} missing key column {}",
4227                                binding.predicate, key_col1
4228                            ),
4229                        }
4230                    })?;
4231                    let key_col2_ref = source_relation.column(key_col2).ok_or_else(|| {
4232                        XlogError::UnsupportedEpistemicConstruct {
4233                            construct: "epistemic GPU stable-model tuple membership".to_string(),
4234                            context: format!(
4235                                "tuple source relation {} missing key column {}",
4236                                binding.predicate, key_col2
4237                            ),
4238                        }
4239                    })?;
4240                    let key_col0_type =
4241                        source_relation
4242                            .schema()
4243                            .column_type(key_col0)
4244                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4245                                construct: "epistemic GPU stable-model tuple membership"
4246                                    .to_string(),
4247                                context: format!(
4248                                    "tuple source relation {} missing schema for key column {}",
4249                                    binding.predicate, key_col0
4250                                ),
4251                            })?;
4252                    let key_col1_type =
4253                        source_relation
4254                            .schema()
4255                            .column_type(key_col1)
4256                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4257                                construct: "epistemic GPU stable-model tuple membership"
4258                                    .to_string(),
4259                                context: format!(
4260                                    "tuple source relation {} missing schema for key column {}",
4261                                    binding.predicate, key_col1
4262                                ),
4263                            })?;
4264                    let key_col2_type =
4265                        source_relation
4266                            .schema()
4267                            .column_type(key_col2)
4268                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4269                                construct: "epistemic GPU stable-model tuple membership"
4270                                    .to_string(),
4271                                context: format!(
4272                                    "tuple source relation {} missing schema for key column {}",
4273                                    binding.predicate, key_col2
4274                                ),
4275                            })?;
4276                    let key_col0_width = key_col0_type.size_bytes();
4277                    let key_col1_width = key_col1_type.size_bytes();
4278                    let key_col2_width = key_col2_type.size_bytes();
4279                    let key_col0_expectation =
4280                        TupleKeyExpectation::from_term(&binding.key_terms[0], key_col0_type)?;
4281                    let key_col1_expectation =
4282                        TupleKeyExpectation::from_term(&binding.key_terms[1], key_col1_type)?;
4283                    let key_col2_expectation =
4284                        TupleKeyExpectation::from_term(&binding.key_terms[2], key_col2_type)?;
4285                    let max_width = key_col0_width.max(key_col1_width).max(key_col2_width);
4286                    if max_width > u32::MAX as usize {
4287                        return Err(XlogError::ResourceExhausted {
4288                            context: "epistemic GPU tuple-key column width".to_string(),
4289                            estimated_bytes: max_width as u64,
4290                            budget_bytes: u32::MAX as u64,
4291                        });
4292                    }
4293                    tuple_sources.push(TupleSourceLaunch::ArityThree {
4294                        literal_index: binding.literal_index as u32,
4295                        reduction_index: binding.reduction_index as u32,
4296                        negated: binding.negated as u8,
4297                        row_count: source_relation.num_rows_device(),
4298                        key_col0: key_col0_ref,
4299                        key_col0_width: key_col0_width as u32,
4300                        expected_key_col0_bits: key_col0_expectation.bits,
4301                        expected_key_col0_type_code: key_col0_expectation.type_code,
4302                        key_col1: key_col1_ref,
4303                        key_col1_width: key_col1_width as u32,
4304                        expected_key_col1_bits: key_col1_expectation.bits,
4305                        expected_key_col1_type_code: key_col1_expectation.type_code,
4306                        key_col2: key_col2_ref,
4307                        key_col2_width: key_col2_width as u32,
4308                        expected_key_col2_bits: key_col2_expectation.bits,
4309                        expected_key_col2_type_code: key_col2_expectation.type_code,
4310                    });
4311                }
4312                key_columns => {
4313                    if key_columns.len() > u32::MAX as usize {
4314                        return Err(XlogError::ResourceExhausted {
4315                            context: "epistemic GPU tuple-key arity".to_string(),
4316                            estimated_bytes: key_columns.len() as u64,
4317                            budget_bytes: u32::MAX as u64,
4318                        });
4319                    }
4320
4321                    let mut key_col_ptrs_host = Vec::with_capacity(key_columns.len());
4322                    let mut key_col_widths_host = Vec::with_capacity(key_columns.len());
4323                    let mut expected_key_bits_host = Vec::with_capacity(key_columns.len());
4324                    let mut expected_key_type_codes_host = Vec::with_capacity(key_columns.len());
4325                    let mut tuple_key_match_modes_host = Vec::with_capacity(key_columns.len());
4326                    let mut bound_value_col_ptrs_host = Vec::with_capacity(key_columns.len());
4327                    let mut bound_value_col_widths_host = Vec::with_capacity(key_columns.len());
4328                    for (term_index, &key_col) in key_columns.iter().enumerate() {
4329                        let key_col_ref = source_relation.column(key_col).ok_or_else(|| {
4330                            XlogError::UnsupportedEpistemicConstruct {
4331                                construct: "epistemic GPU stable-model tuple membership"
4332                                    .to_string(),
4333                                context: format!(
4334                                    "tuple source relation {} missing key column {}",
4335                                    binding.predicate, key_col
4336                                ),
4337                            }
4338                        })?;
4339                        let key_col_type = source_relation
4340                            .schema()
4341                            .column_type(key_col)
4342                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4343                                construct: "epistemic GPU stable-model tuple membership"
4344                                    .to_string(),
4345                                context: format!(
4346                                    "tuple source relation {} missing schema for key column {}",
4347                                    binding.predicate, key_col
4348                                ),
4349                            })?;
4350                        let key_col_width = key_col_type.size_bytes();
4351                        if key_col_width > u32::MAX as usize {
4352                            return Err(XlogError::ResourceExhausted {
4353                                context: "epistemic GPU tuple-key column width".to_string(),
4354                                estimated_bytes: key_col_width as u64,
4355                                budget_bytes: u32::MAX as u64,
4356                            });
4357                        }
4358
4359                        key_col_ptrs_host.push(*key_col_ref.device_ptr());
4360                        key_col_widths_host.push(key_col_width as u32);
4361                        match &binding.key_terms[term_index] {
4362                            EirTerm::Variable(variable_name) => {
4363                                let bound_col_index = binding.bound_output_columns[term_index]
4364                                    .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
4365                                        construct: "epistemic GPU bound tuple-key matching"
4366                                            .to_string(),
4367                                        context: format!(
4368                                            "tuple key variable {variable_name} has no reduced \
4369                                             output column binding"
4370                                        ),
4371                                    })?;
4372                                let bound_col =
4373                                    output.column(bound_col_index).ok_or_else(|| {
4374                                        XlogError::UnsupportedEpistemicConstruct {
4375                                            construct: "epistemic GPU bound tuple-key matching"
4376                                                .to_string(),
4377                                            context: format!(
4378                                                "reduced output is missing device column \
4379                                             {bound_col_index} for variable {variable_name}"
4380                                            ),
4381                                        }
4382                                    })?;
4383                                let bound_col_type =
4384                                    output.schema().column_type(bound_col_index).ok_or_else(
4385                                        || XlogError::UnsupportedEpistemicConstruct {
4386                                            construct: "epistemic GPU bound tuple-key matching"
4387                                                .to_string(),
4388                                            context: format!(
4389                                                "reduced output is missing schema for variable \
4390                                             {variable_name}"
4391                                            ),
4392                                        },
4393                                    )?;
4394                                if bound_col_type != key_col_type {
4395                                    return Err(XlogError::UnsupportedEpistemicConstruct {
4396                                        construct: "epistemic GPU bound tuple-key matching"
4397                                            .to_string(),
4398                                        context: format!(
4399                                            "bound variable {variable_name} has output type \
4400                                             {bound_col_type:?}, but tuple source {} key column \
4401                                             {} has type {key_col_type:?}",
4402                                            binding.predicate, key_col
4403                                        ),
4404                                    });
4405                                }
4406                                let bound_col_width = bound_col_type.size_bytes();
4407                                if bound_col_width > u32::MAX as usize {
4408                                    return Err(XlogError::ResourceExhausted {
4409                                        context: "epistemic GPU bound tuple-key column width"
4410                                            .to_string(),
4411                                        estimated_bytes: bound_col_width as u64,
4412                                        budget_bytes: u32::MAX as u64,
4413                                    });
4414                                }
4415
4416                                expected_key_bits_host.push(0);
4417                                expected_key_type_codes_host.push(key_col_type.to_code());
4418                                tuple_key_match_modes_host.push(TUPLE_KEY_MATCH_MODE_BOUND_OUTPUT);
4419                                bound_value_col_ptrs_host.push(*bound_col.device_ptr());
4420                                bound_value_col_widths_host.push(bound_col_width as u32);
4421                            }
4422                            EirTerm::Anonymous => {
4423                                // Wildcard: no equality requirement on this
4424                                // tuple-key column. The device still reads the
4425                                // column pointer/width, but the kernel matches
4426                                // every stable-model value in this position.
4427                                expected_key_bits_host.push(0);
4428                                expected_key_type_codes_host.push(key_col_type.to_code());
4429                                tuple_key_match_modes_host.push(TUPLE_KEY_MATCH_MODE_WILDCARD);
4430                                bound_value_col_ptrs_host.push(0);
4431                                bound_value_col_widths_host.push(0);
4432                            }
4433                            term => {
4434                                let expectation =
4435                                    TupleKeyExpectation::from_term(term, key_col_type)?;
4436                                expected_key_bits_host.push(expectation.bits);
4437                                expected_key_type_codes_host.push(expectation.type_code);
4438                                tuple_key_match_modes_host.push(TUPLE_KEY_MATCH_MODE_GROUND);
4439                                bound_value_col_ptrs_host.push(0);
4440                                bound_value_col_widths_host.push(0);
4441                            }
4442                        }
4443                    }
4444
4445                    let memory = self.provider.memory();
4446                    let mut key_col_ptrs = memory.alloc::<u64>(key_columns.len())?;
4447                    let mut key_col_widths = memory.alloc::<u32>(key_columns.len())?;
4448                    let mut expected_key_bits = memory.alloc::<u64>(key_columns.len())?;
4449                    let mut expected_key_type_codes = memory.alloc::<u8>(key_columns.len())?;
4450                    let mut tuple_key_match_modes = memory.alloc::<u8>(key_columns.len())?;
4451                    let mut bound_value_col_ptrs = memory.alloc::<u64>(key_columns.len())?;
4452                    let mut bound_value_col_widths = memory.alloc::<u32>(key_columns.len())?;
4453                    self.provider
4454                        .htod_launch_metadata_sync_copy_into(&key_col_ptrs_host, &mut key_col_ptrs)
4455                        .map_err(|e| {
4456                            XlogError::execution_ctx(
4457                                "epistemic GPU tuple-key metadata",
4458                                "upload key column pointers",
4459                                &e,
4460                            )
4461                        })?;
4462                    self.provider
4463                        .htod_launch_metadata_sync_copy_into(
4464                            &key_col_widths_host,
4465                            &mut key_col_widths,
4466                        )
4467                        .map_err(|e| {
4468                            XlogError::execution_ctx(
4469                                "epistemic GPU tuple-key metadata",
4470                                "upload key column widths",
4471                                &e,
4472                            )
4473                        })?;
4474                    self.provider
4475                        .htod_launch_metadata_sync_copy_into(
4476                            &expected_key_bits_host,
4477                            &mut expected_key_bits,
4478                        )
4479                        .map_err(|e| {
4480                            XlogError::execution_ctx(
4481                                "epistemic GPU tuple-key metadata",
4482                                "upload expected key bits",
4483                                &e,
4484                            )
4485                        })?;
4486                    self.provider
4487                        .htod_launch_metadata_sync_copy_into(
4488                            &expected_key_type_codes_host,
4489                            &mut expected_key_type_codes,
4490                        )
4491                        .map_err(|e| {
4492                            XlogError::execution_ctx(
4493                                "epistemic GPU tuple-key metadata",
4494                                "upload expected key type codes",
4495                                &e,
4496                            )
4497                        })?;
4498                    self.provider
4499                        .htod_launch_metadata_sync_copy_into(
4500                            &tuple_key_match_modes_host,
4501                            &mut tuple_key_match_modes,
4502                        )
4503                        .map_err(|e| {
4504                            XlogError::execution_ctx(
4505                                "epistemic GPU tuple-key metadata",
4506                                "upload tuple key match modes",
4507                                &e,
4508                            )
4509                        })?;
4510                    self.provider
4511                        .htod_launch_metadata_sync_copy_into(
4512                            &bound_value_col_ptrs_host,
4513                            &mut bound_value_col_ptrs,
4514                        )
4515                        .map_err(|e| {
4516                            XlogError::execution_ctx(
4517                                "epistemic GPU tuple-key metadata",
4518                                "upload bound value column pointers",
4519                                &e,
4520                            )
4521                        })?;
4522                    self.provider
4523                        .htod_launch_metadata_sync_copy_into(
4524                            &bound_value_col_widths_host,
4525                            &mut bound_value_col_widths,
4526                        )
4527                        .map_err(|e| {
4528                            XlogError::execution_ctx(
4529                                "epistemic GPU tuple-key metadata",
4530                                "upload bound value column widths",
4531                                &e,
4532                            )
4533                        })?;
4534
4535                    tuple_sources.push(TupleSourceLaunch::ArityN {
4536                        literal_index: binding.literal_index as u32,
4537                        reduction_index: binding.reduction_index as u32,
4538                        negated: binding.negated as u8,
4539                        row_count: source_relation.num_rows_device(),
4540                        bound_value_row_count: output.num_rows_device(),
4541                        key_col_count: key_columns.len() as u32,
4542                        key_col_ptrs,
4543                        key_col_widths,
4544                        expected_key_bits,
4545                        expected_key_type_codes,
4546                        tuple_key_match_modes,
4547                        bound_value_col_ptrs,
4548                        bound_value_col_widths,
4549                        has_bound_value_keys: has_bound_value_keys as u8,
4550                    });
4551                }
4552            }
4553        }
4554
4555        let literal_count = literal_count as u32;
4556        let candidate_count = candidate_count as u32;
4557        let reduction_count = reduction_count as u32;
4558        let models_per_reduction = models_per_reduction as u32;
4559        let world_stride = world_stride as u32;
4560        let func = self
4561            .provider
4562            .device()
4563            .inner()
4564            .get_func(
4565                EPISTEMIC_MODULE,
4566                epistemic_kernels::EPISTEMIC_POPULATE_MODEL_MEMBERSHIP_FROM_TUPLE_SOURCE_U8,
4567            )
4568            .ok_or_else(|| {
4569                XlogError::Execution(
4570                    "epistemic tuple-source model-membership kernel not found".to_string(),
4571                )
4572            })?;
4573        let func_arity1 = self
4574            .provider
4575            .device()
4576            .inner()
4577            .get_func(
4578                EPISTEMIC_MODULE,
4579                epistemic_kernels::EPISTEMIC_POPULATE_MODEL_MEMBERSHIP_FROM_TUPLE_SOURCE_ARITY1_U8,
4580            )
4581            .ok_or_else(|| {
4582                XlogError::Execution(
4583                    "epistemic arity-one tuple-source model-membership kernel not found"
4584                        .to_string(),
4585                )
4586            })?;
4587        let func_arity2 = self
4588            .provider
4589            .device()
4590            .inner()
4591            .get_func(
4592                EPISTEMIC_MODULE,
4593                epistemic_kernels::EPISTEMIC_POPULATE_MODEL_MEMBERSHIP_FROM_TUPLE_SOURCE_ARITY2_U8,
4594            )
4595            .ok_or_else(|| {
4596                XlogError::Execution(
4597                    "epistemic arity-two tuple-source model-membership kernel not found"
4598                        .to_string(),
4599                )
4600            })?;
4601        let func_arity3 = self
4602            .provider
4603            .device()
4604            .inner()
4605            .get_func(
4606                EPISTEMIC_MODULE,
4607                epistemic_kernels::EPISTEMIC_POPULATE_MODEL_MEMBERSHIP_FROM_TUPLE_SOURCE_ARITY3_U8,
4608            )
4609            .ok_or_else(|| {
4610                XlogError::Execution(
4611                    "epistemic arity-three tuple-source model-membership kernel not found"
4612                        .to_string(),
4613                )
4614            })?;
4615        let func_arity_n = self
4616            .provider
4617            .device()
4618            .inner()
4619            .get_func(
4620                EPISTEMIC_MODULE,
4621                epistemic_kernels::EPISTEMIC_POPULATE_MODEL_MEMBERSHIP_FROM_TUPLE_SOURCE_ARITY_N_U8,
4622            )
4623            .ok_or_else(|| {
4624                XlogError::Execution(
4625                    "epistemic generic-arity tuple-source model-membership kernel not found"
4626                        .to_string(),
4627                )
4628            })?;
4629        let config = LaunchConfig::for_num_elems(per_binding_launch_elems as u32);
4630
4631        let mut kernel_timings = Vec::with_capacity(tuple_sources.len());
4632        for tuple_source in &tuple_sources {
4633            let kernel_timing = self.time_epistemic_gpu_kernel_launch(
4634                "epistemic GPU tuple-source model membership",
4635                || unsafe {
4636                    match tuple_source {
4637                        TupleSourceLaunch::ArityZero {
4638                            literal_index,
4639                            reduction_index,
4640                            negated,
4641                            row_count,
4642                        } => {
4643                            // SAFETY: kernel arguments match the PTX signature; the capacity
4644                            // checks above prove candidate, world-view, membership, rejection,
4645                            // and tuple-source row-count buffers cover all accesses.
4646                            let mut params: Vec<*mut c_void> = vec![
4647                                literal_count.as_kernel_param(),
4648                                candidate_count.as_kernel_param(),
4649                                reduction_count.as_kernel_param(),
4650                                models_per_reduction.as_kernel_param(),
4651                                world_stride.as_kernel_param(),
4652                                literal_index.as_kernel_param(),
4653                                reduction_index.as_kernel_param(),
4654                                negated.as_kernel_param(),
4655                                output.num_rows_device().as_kernel_param(),
4656                                row_count.as_kernel_param(),
4657                                (&workspace.candidate_assumptions).as_kernel_param(),
4658                                (&workspace.world_views).as_kernel_param(),
4659                                (&workspace.model_membership).as_kernel_param(),
4660                                (&workspace.rejection_reasons).as_kernel_param(),
4661                            ];
4662                            func.clone().launch(config, &mut params)?;
4663                        }
4664                        TupleSourceLaunch::ArityOne {
4665                            literal_index,
4666                            reduction_index,
4667                            negated,
4668                            row_count,
4669                            key_col0,
4670                            key_col0_width,
4671                            expected_key_col0_bits,
4672                            expected_key_col0_type_code,
4673                        } => {
4674                            // SAFETY: kernel arguments match the PTX signature; capacity checks
4675                            // above cover workspace buffers, row_count comes from the named source
4676                            // relation, and key_col0/key_col0_width are schema-validated.
4677                            let mut params: Vec<*mut c_void> = vec![
4678                                literal_count.as_kernel_param(),
4679                                candidate_count.as_kernel_param(),
4680                                reduction_count.as_kernel_param(),
4681                                models_per_reduction.as_kernel_param(),
4682                                world_stride.as_kernel_param(),
4683                                literal_index.as_kernel_param(),
4684                                reduction_index.as_kernel_param(),
4685                                negated.as_kernel_param(),
4686                                output.num_rows_device().as_kernel_param(),
4687                                row_count.as_kernel_param(),
4688                                key_col0.as_kernel_param(),
4689                                key_col0_width.as_kernel_param(),
4690                                expected_key_col0_bits.as_kernel_param(),
4691                                expected_key_col0_type_code.as_kernel_param(),
4692                                (&workspace.candidate_assumptions).as_kernel_param(),
4693                                (&workspace.world_views).as_kernel_param(),
4694                                (&workspace.model_membership).as_kernel_param(),
4695                                (&workspace.rejection_reasons).as_kernel_param(),
4696                            ];
4697                            func_arity1.clone().launch(config, &mut params)?;
4698                        }
4699                        TupleSourceLaunch::ArityTwo {
4700                            literal_index,
4701                            reduction_index,
4702                            negated,
4703                            row_count,
4704                            key_col0,
4705                            key_col0_width,
4706                            expected_key_col0_bits,
4707                            expected_key_col0_type_code,
4708                            key_col1,
4709                            key_col1_width,
4710                            expected_key_col1_bits,
4711                            expected_key_col1_type_code,
4712                        } => {
4713                            // SAFETY: kernel arguments match the PTX signature; capacity checks
4714                            // above cover workspace buffers, row_count comes from the named source
4715                            // relation, and both key columns are schema-validated.
4716                            let mut params: Vec<*mut c_void> = vec![
4717                                literal_count.as_kernel_param(),
4718                                candidate_count.as_kernel_param(),
4719                                reduction_count.as_kernel_param(),
4720                                models_per_reduction.as_kernel_param(),
4721                                world_stride.as_kernel_param(),
4722                                literal_index.as_kernel_param(),
4723                                reduction_index.as_kernel_param(),
4724                                negated.as_kernel_param(),
4725                                output.num_rows_device().as_kernel_param(),
4726                                row_count.as_kernel_param(),
4727                                key_col0.as_kernel_param(),
4728                                key_col0_width.as_kernel_param(),
4729                                expected_key_col0_bits.as_kernel_param(),
4730                                expected_key_col0_type_code.as_kernel_param(),
4731                                key_col1.as_kernel_param(),
4732                                key_col1_width.as_kernel_param(),
4733                                expected_key_col1_bits.as_kernel_param(),
4734                                expected_key_col1_type_code.as_kernel_param(),
4735                                (&workspace.candidate_assumptions).as_kernel_param(),
4736                                (&workspace.world_views).as_kernel_param(),
4737                                (&workspace.model_membership).as_kernel_param(),
4738                                (&workspace.rejection_reasons).as_kernel_param(),
4739                            ];
4740                            func_arity2.clone().launch(config, &mut params)?;
4741                        }
4742                        TupleSourceLaunch::ArityThree {
4743                            literal_index,
4744                            reduction_index,
4745                            negated,
4746                            row_count,
4747                            key_col0,
4748                            key_col0_width,
4749                            expected_key_col0_bits,
4750                            expected_key_col0_type_code,
4751                            key_col1,
4752                            key_col1_width,
4753                            expected_key_col1_bits,
4754                            expected_key_col1_type_code,
4755                            key_col2,
4756                            key_col2_width,
4757                            expected_key_col2_bits,
4758                            expected_key_col2_type_code,
4759                        } => {
4760                            // SAFETY: kernel arguments match the PTX signature; capacity checks
4761                            // above cover workspace buffers, row_count comes from the named source
4762                            // relation, and all key columns are schema-validated.
4763                            let mut params: Vec<*mut c_void> = vec![
4764                                literal_count.as_kernel_param(),
4765                                candidate_count.as_kernel_param(),
4766                                reduction_count.as_kernel_param(),
4767                                models_per_reduction.as_kernel_param(),
4768                                world_stride.as_kernel_param(),
4769                                literal_index.as_kernel_param(),
4770                                reduction_index.as_kernel_param(),
4771                                negated.as_kernel_param(),
4772                                output.num_rows_device().as_kernel_param(),
4773                                row_count.as_kernel_param(),
4774                                key_col0.as_kernel_param(),
4775                                key_col0_width.as_kernel_param(),
4776                                expected_key_col0_bits.as_kernel_param(),
4777                                expected_key_col0_type_code.as_kernel_param(),
4778                                key_col1.as_kernel_param(),
4779                                key_col1_width.as_kernel_param(),
4780                                expected_key_col1_bits.as_kernel_param(),
4781                                expected_key_col1_type_code.as_kernel_param(),
4782                                key_col2.as_kernel_param(),
4783                                key_col2_width.as_kernel_param(),
4784                                expected_key_col2_bits.as_kernel_param(),
4785                                expected_key_col2_type_code.as_kernel_param(),
4786                                (&workspace.candidate_assumptions).as_kernel_param(),
4787                                (&workspace.world_views).as_kernel_param(),
4788                                (&workspace.model_membership).as_kernel_param(),
4789                                (&workspace.rejection_reasons).as_kernel_param(),
4790                            ];
4791                            func_arity3.clone().launch(config, &mut params)?;
4792                        }
4793                        TupleSourceLaunch::ArityN {
4794                            literal_index,
4795                            reduction_index,
4796                            negated,
4797                            row_count,
4798                            bound_value_row_count,
4799                            key_col_count,
4800                            key_col_ptrs,
4801                            key_col_widths,
4802                            expected_key_bits,
4803                            expected_key_type_codes,
4804                            tuple_key_match_modes,
4805                            bound_value_col_ptrs,
4806                            bound_value_col_widths,
4807                            has_bound_value_keys,
4808                        } => {
4809                            // SAFETY: kernel arguments match the PTX signature; capacity checks
4810                            // above cover workspace buffers, row_count comes from the named source
4811                            // relation, and pointer/width/expectation arrays are device-resident
4812                            // launch metadata for existing relation and reduced-output columns.
4813                            let mut params: Vec<*mut c_void> = vec![
4814                                literal_count.as_kernel_param(),
4815                                candidate_count.as_kernel_param(),
4816                                reduction_count.as_kernel_param(),
4817                                models_per_reduction.as_kernel_param(),
4818                                world_stride.as_kernel_param(),
4819                                literal_index.as_kernel_param(),
4820                                reduction_index.as_kernel_param(),
4821                                negated.as_kernel_param(),
4822                                output.num_rows_device().as_kernel_param(),
4823                                row_count.as_kernel_param(),
4824                                key_col_ptrs.as_kernel_param(),
4825                                key_col_widths.as_kernel_param(),
4826                                expected_key_bits.as_kernel_param(),
4827                                expected_key_type_codes.as_kernel_param(),
4828                                tuple_key_match_modes.as_kernel_param(),
4829                                bound_value_col_ptrs.as_kernel_param(),
4830                                bound_value_col_widths.as_kernel_param(),
4831                                bound_value_row_count.as_kernel_param(),
4832                                key_col_count.as_kernel_param(),
4833                                has_bound_value_keys.as_kernel_param(),
4834                                (&workspace.candidate_assumptions).as_kernel_param(),
4835                                (&workspace.world_views).as_kernel_param(),
4836                                (&workspace.model_membership).as_kernel_param(),
4837                                (&workspace.rejection_reasons).as_kernel_param(),
4838                            ];
4839                            func_arity_n.clone().launch(config, &mut params)?;
4840                        }
4841                    };
4842                    Ok(())
4843                },
4844            )?;
4845            kernel_timings.push(kernel_timing);
4846        }
4847        let kernel_timing = EpistemicGpuKernelTimingTrace::checked_sum(kernel_timings)?;
4848
4849        Ok(trace.with_kernel_timing(kernel_timing))
4850    }
4851
4852    /// Validate staged model memberships against candidate world views on device.
4853    pub fn validate_epistemic_gpu_world_views(
4854        &self,
4855        workspace: &mut EpistemicGpuWorkspace,
4856        gpu_plan: &EpistemicGpuPlan,
4857        candidate_count: usize,
4858        models_per_reduction: usize,
4859    ) -> Result<EpistemicGpuWorldViewValidationTrace> {
4860        gpu_plan.validate_tuple_membership_bindings()?;
4861        let literal_count = gpu_plan.epistemic_literals.len();
4862        let reduction_count = gpu_plan.reductions.len();
4863        let trace = EpistemicGpuWorldViewValidationTrace::for_counts(
4864            literal_count,
4865            candidate_count,
4866            reduction_count,
4867            models_per_reduction,
4868        )?;
4869        if trace.model_membership_bytes_checked > workspace.layout.model_membership_bytes {
4870            return Err(XlogError::ResourceExhausted {
4871                context: "epistemic GPU world-view validation membership workspace".to_string(),
4872                estimated_bytes: trace.model_membership_bytes_checked as u64,
4873                budget_bytes: workspace.layout.model_membership_bytes as u64,
4874            });
4875        }
4876        if trace.world_view_slots_checked > workspace.layout.world_view_bytes {
4877            return Err(XlogError::ResourceExhausted {
4878                context: "epistemic GPU world-view validation world-view workspace".to_string(),
4879                estimated_bytes: trace.world_view_slots_checked as u64,
4880                budget_bytes: workspace.layout.world_view_bytes as u64,
4881            });
4882        }
4883        if trace.rejection_reason_slots_written > workspace.layout.rejection_reason_slots {
4884            return Err(XlogError::ResourceExhausted {
4885                context: "epistemic GPU world-view validation rejection workspace".to_string(),
4886                estimated_bytes: trace.rejection_reason_slots_written as u64,
4887                budget_bytes: workspace.layout.rejection_reason_slots as u64,
4888            });
4889        }
4890        if trace.model_membership_bytes_checked > u32::MAX as usize {
4891            return Err(XlogError::ResourceExhausted {
4892                context: "epistemic GPU world-view validation membership launch".to_string(),
4893                estimated_bytes: trace.model_membership_bytes_checked as u64,
4894                budget_bytes: u32::MAX as u64,
4895            });
4896        }
4897        if literal_count > u32::MAX as usize
4898            || candidate_count > u32::MAX as usize
4899            || reduction_count > u32::MAX as usize
4900            || models_per_reduction > u32::MAX as usize
4901        {
4902            return Err(XlogError::ResourceExhausted {
4903                context: "epistemic GPU world-view validation dimensions".to_string(),
4904                estimated_bytes: literal_count
4905                    .max(candidate_count)
4906                    .max(reduction_count)
4907                    .max(models_per_reduction) as u64,
4908                budget_bytes: u32::MAX as u64,
4909            });
4910        }
4911
4912        let mut literal_op_codes_host = vec![0u8; literal_count];
4913        let mut literal_negated_host = vec![0u8; literal_count];
4914        let mut literal_bound_to_output_host = vec![0u8; literal_count];
4915        let mut literal_reduction_indices_host = vec![0u32; literal_count];
4916        for binding in &gpu_plan.tuple_membership_bindings {
4917            literal_op_codes_host[binding.literal_index] = epistemic_operator_code(binding.op);
4918            literal_negated_host[binding.literal_index] = u8::from(binding.negated);
4919            literal_bound_to_output_host[binding.literal_index] =
4920                u8::from(binding.bound_output_columns.iter().any(Option::is_some));
4921            literal_reduction_indices_host[binding.literal_index] = binding.reduction_index as u32;
4922        }
4923        let memory = self.provider.memory();
4924        let mut literal_op_codes = memory.alloc::<u8>(literal_count)?;
4925        let mut literal_negated = memory.alloc::<u8>(literal_count)?;
4926        let mut literal_bound_to_output = memory.alloc::<u8>(literal_count)?;
4927        let mut literal_reduction_indices = memory.alloc::<u32>(literal_count)?;
4928        self.provider
4929            .htod_launch_metadata_sync_copy_into(&literal_op_codes_host, &mut literal_op_codes)
4930            .map_err(|e| {
4931                XlogError::execution_ctx(
4932                    "epistemic GPU world-view validation metadata",
4933                    "upload literal operator codes",
4934                    &e,
4935                )
4936            })?;
4937        self.provider
4938            .htod_launch_metadata_sync_copy_into(&literal_negated_host, &mut literal_negated)
4939            .map_err(|e| {
4940                XlogError::execution_ctx(
4941                    "epistemic GPU world-view validation metadata",
4942                    "upload literal negation flags",
4943                    &e,
4944                )
4945            })?;
4946        self.provider
4947            .htod_launch_metadata_sync_copy_into(
4948                &literal_bound_to_output_host,
4949                &mut literal_bound_to_output,
4950            )
4951            .map_err(|e| {
4952                XlogError::execution_ctx(
4953                    "epistemic GPU world-view validation metadata",
4954                    "upload literal output-binding flags",
4955                    &e,
4956                )
4957            })?;
4958        self.provider
4959            .htod_launch_metadata_sync_copy_into(
4960                &literal_reduction_indices_host,
4961                &mut literal_reduction_indices,
4962            )
4963            .map_err(|e| {
4964                XlogError::execution_ctx(
4965                    "epistemic GPU world-view validation metadata",
4966                    "upload literal reduction indices",
4967                    &e,
4968                )
4969            })?;
4970
4971        let world_stride =
4972            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
4973        if world_stride == 0 || world_stride > u32::MAX as usize {
4974            return Err(XlogError::ResourceExhausted {
4975                context: "epistemic GPU world-view validation world stride".to_string(),
4976                estimated_bytes: world_stride as u64,
4977                budget_bytes: u32::MAX as u64,
4978            });
4979        }
4980
4981        let literal_count = literal_count as u32;
4982        let candidate_count = candidate_count as u32;
4983        let reduction_count = reduction_count as u32;
4984        let models_per_reduction = models_per_reduction as u32;
4985        let world_stride = world_stride as u32;
4986        let func = self
4987            .provider
4988            .device()
4989            .inner()
4990            .get_func(
4991                EPISTEMIC_MODULE,
4992                epistemic_kernels::EPISTEMIC_VALIDATE_WORLD_VIEWS_U8,
4993            )
4994            .ok_or_else(|| {
4995                XlogError::Execution("epistemic world-view validation kernel not found".to_string())
4996            })?;
4997        let config = LaunchConfig::for_num_elems(candidate_count);
4998
4999        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
5000            "epistemic GPU world-view validation",
5001            || unsafe {
5002                // SAFETY: kernel arguments match the PTX signature; the capacity checks
5003                // above prove model-membership, world-view, and rejection buffers cover
5004                // all reads and writes for the candidate range.
5005                let mut params: Vec<*mut c_void> = vec![
5006                    literal_count.as_kernel_param(),
5007                    candidate_count.as_kernel_param(),
5008                    reduction_count.as_kernel_param(),
5009                    models_per_reduction.as_kernel_param(),
5010                    world_stride.as_kernel_param(),
5011                    (&literal_op_codes).as_kernel_param(),
5012                    (&literal_negated).as_kernel_param(),
5013                    (&literal_bound_to_output).as_kernel_param(),
5014                    (&literal_reduction_indices).as_kernel_param(),
5015                    (&workspace.candidate_assumptions).as_kernel_param(),
5016                    (&workspace.model_membership).as_kernel_param(),
5017                    (&workspace.world_views).as_kernel_param(),
5018                    (&workspace.rejection_reasons).as_kernel_param(),
5019                ];
5020                func.clone().launch(config, &mut params)
5021            },
5022        )?;
5023
5024        Ok(trace.with_kernel_timing(kernel_timing))
5025    }
5026
5027    /// Prune accepted candidate world views that satisfy an epistemic integrity
5028    /// constraint body.
5029    ///
5030    /// Runs after [`Self::validate_epistemic_gpu_world_views`]: each surviving
5031    /// candidate's assumption bit equals the negation-folded observed modal
5032    /// value of its literal, so a constraint body holds in this accepted world
5033    /// view exactly when every referenced literal's assumption bit is set. Such
5034    /// candidates are pruned on device with the world-view constraint-violation
5035    /// rejection code; no accepted world is read back to the host.
5036    pub fn validate_epistemic_gpu_world_view_constraints(
5037        &self,
5038        workspace: &mut EpistemicGpuWorkspace,
5039        gpu_plan: &EpistemicGpuPlan,
5040        candidate_count: usize,
5041    ) -> Result<EpistemicGpuConstraintWorldViewValidationTrace> {
5042        gpu_plan.validate_constraints()?;
5043        let literal_count = gpu_plan.epistemic_literals.len();
5044        let constraint_count = gpu_plan.constraints.len();
5045
5046        // Initialize the parallel constraint-violation index buffer to the
5047        // sentinel `u32::MAX` ("not rejected by a constraint") for every
5048        // candidate, BEFORE the zero-constraint early return below. Zero is a
5049        // valid constraint index, so the buffer cannot be left zeroed: any
5050        // candidate rejected by reason codes 1-5 (or accepted) must read back as
5051        // the sentinel, never a spurious `Some(0)`. The upload rides the
5052        // launch-metadata channel (like the CSR buffers below), so it adds no
5053        // tracked data-plane host-to-device transfer and keeps `host_write_ops` at zero.
5054        if candidate_count > workspace.layout.rejection_reason_slots {
5055            return Err(XlogError::ResourceExhausted {
5056                context: "epistemic GPU constraint-violation index workspace".to_string(),
5057                estimated_bytes: candidate_count as u64,
5058                budget_bytes: workspace.layout.rejection_reason_slots as u64,
5059            });
5060        }
5061        if candidate_count > 0 {
5062            let sentinel_host = vec![u32::MAX; candidate_count];
5063            let fill_len = candidate_count;
5064            let mut sentinel_view = workspace.constraint_violation_index.slice_mut(0..fill_len);
5065            self.provider
5066                .htod_launch_metadata_sync_copy_into(&sentinel_host, &mut sentinel_view)
5067                .map_err(|e| {
5068                    XlogError::execution_ctx(
5069                        "epistemic GPU world-view constraint metadata",
5070                        "initialize constraint-violation index sentinel",
5071                        &e,
5072                    )
5073                })?;
5074        }
5075
5076        // Flatten constraint -> literal index references into CSR-style buffers.
5077        let mut offsets_host = Vec::with_capacity(constraint_count);
5078        let mut counts_host = Vec::with_capacity(constraint_count);
5079        let mut indices_host: Vec<u32> = Vec::new();
5080        for constraint in &gpu_plan.constraints {
5081            offsets_host.push(indices_host.len() as u32);
5082            counts_host.push(constraint.literal_indices.len() as u32);
5083            for &literal_index in &constraint.literal_indices {
5084                indices_host.push(literal_index as u32);
5085            }
5086        }
5087        let constraint_literal_refs = indices_host.len();
5088
5089        let trace = EpistemicGpuConstraintWorldViewValidationTrace {
5090            constraint_count,
5091            constraint_literal_refs,
5092            candidates_checked: candidate_count,
5093            rejection_reason_slots_written: candidate_count,
5094            kernel_launches: 0,
5095            host_write_ops: 0,
5096            kernel_timing: EpistemicGpuKernelTimingTrace::unrecorded(),
5097        };
5098
5099        if constraint_count == 0 {
5100            // No world-view constraints to evaluate; leave the rejection buffer
5101            // untouched so accepted candidates flow through unchanged.
5102            return Ok(trace);
5103        }
5104
5105        if candidate_count > workspace.layout.rejection_reason_slots {
5106            return Err(XlogError::ResourceExhausted {
5107                context: "epistemic GPU world-view constraint rejection workspace".to_string(),
5108                estimated_bytes: candidate_count as u64,
5109                budget_bytes: workspace.layout.rejection_reason_slots as u64,
5110            });
5111        }
5112        if candidate_count > u32::MAX as usize
5113            || literal_count > u32::MAX as usize
5114            || constraint_count > u32::MAX as usize
5115            || constraint_literal_refs > u32::MAX as usize
5116        {
5117            return Err(XlogError::ResourceExhausted {
5118                context: "epistemic GPU world-view constraint dimensions".to_string(),
5119                estimated_bytes: candidate_count
5120                    .max(literal_count)
5121                    .max(constraint_count)
5122                    .max(constraint_literal_refs) as u64,
5123                budget_bytes: u32::MAX as u64,
5124            });
5125        }
5126
5127        let memory = self.provider.memory();
5128        let mut constraint_literal_offsets = memory.alloc::<u32>(constraint_count)?;
5129        let mut constraint_literal_counts = memory.alloc::<u32>(constraint_count)?;
5130        let mut constraint_literal_indices = memory.alloc::<u32>(constraint_literal_refs.max(1))?;
5131        self.provider
5132            .htod_launch_metadata_sync_copy_into(&offsets_host, &mut constraint_literal_offsets)
5133            .map_err(|e| {
5134                XlogError::execution_ctx(
5135                    "epistemic GPU world-view constraint metadata",
5136                    "upload constraint literal offsets",
5137                    &e,
5138                )
5139            })?;
5140        self.provider
5141            .htod_launch_metadata_sync_copy_into(&counts_host, &mut constraint_literal_counts)
5142            .map_err(|e| {
5143                XlogError::execution_ctx(
5144                    "epistemic GPU world-view constraint metadata",
5145                    "upload constraint literal counts",
5146                    &e,
5147                )
5148            })?;
5149        if !indices_host.is_empty() {
5150            self.provider
5151                .htod_launch_metadata_sync_copy_into(&indices_host, &mut constraint_literal_indices)
5152                .map_err(|e| {
5153                    XlogError::execution_ctx(
5154                        "epistemic GPU world-view constraint metadata",
5155                        "upload constraint literal indices",
5156                        &e,
5157                    )
5158                })?;
5159        }
5160
5161        let literal_count_u32 = literal_count as u32;
5162        let candidate_count_u32 = candidate_count as u32;
5163        let constraint_count_u32 = constraint_count as u32;
5164        let func = self
5165            .provider
5166            .device()
5167            .inner()
5168            .get_func(
5169                EPISTEMIC_MODULE,
5170                epistemic_kernels::EPISTEMIC_VALIDATE_CONSTRAINTS_U8,
5171            )
5172            .ok_or_else(|| {
5173                XlogError::Execution(
5174                    "epistemic world-view constraint validation kernel not found".to_string(),
5175                )
5176            })?;
5177        let config = LaunchConfig::for_num_elems(candidate_count_u32);
5178
5179        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
5180            "epistemic GPU world-view constraint validation",
5181            || unsafe {
5182                // SAFETY: kernel arguments match the PTX signature; the capacity
5183                // check above proves the rejection buffer covers every candidate,
5184                // and CSR offset/count/index buffers are sized to the constraint
5185                // literal references uploaded above.
5186                let mut params: Vec<*mut c_void> = vec![
5187                    literal_count_u32.as_kernel_param(),
5188                    candidate_count_u32.as_kernel_param(),
5189                    constraint_count_u32.as_kernel_param(),
5190                    (&constraint_literal_offsets).as_kernel_param(),
5191                    (&constraint_literal_counts).as_kernel_param(),
5192                    (&constraint_literal_indices).as_kernel_param(),
5193                    (&workspace.candidate_assumptions).as_kernel_param(),
5194                    (&mut workspace.rejection_reasons).as_kernel_param(),
5195                    (&mut workspace.constraint_violation_index).as_kernel_param(),
5196                ];
5197                func.clone().launch(config, &mut params)
5198            },
5199        )?;
5200
5201        Ok(EpistemicGpuConstraintWorldViewValidationTrace {
5202            kernel_launches: 1,
5203            kernel_timing,
5204            ..trace
5205        })
5206    }
5207
5208    /// Materialize accepted candidate flags into the GPU world-view buffer.
5209    pub fn materialize_epistemic_gpu_candidates(
5210        &self,
5211        workspace: &mut EpistemicGpuWorkspace,
5212        candidate_count: usize,
5213    ) -> Result<EpistemicGpuMaterializationTrace> {
5214        let trace = EpistemicGpuMaterializationTrace::for_count(candidate_count)?;
5215        if trace.world_view_slots_written > workspace.layout.world_view_bytes {
5216            return Err(XlogError::ResourceExhausted {
5217                context: "epistemic GPU materialization world-view workspace".to_string(),
5218                estimated_bytes: trace.world_view_slots_written as u64,
5219                budget_bytes: workspace.layout.world_view_bytes as u64,
5220            });
5221        }
5222        if candidate_count > workspace.layout.rejection_reason_slots {
5223            return Err(XlogError::ResourceExhausted {
5224                context: "epistemic GPU materialization rejection workspace".to_string(),
5225                estimated_bytes: candidate_count as u64,
5226                budget_bytes: workspace.layout.rejection_reason_slots as u64,
5227            });
5228        }
5229        if candidate_count > u32::MAX as usize {
5230            return Err(XlogError::ResourceExhausted {
5231                context: "epistemic GPU materialization launch".to_string(),
5232                estimated_bytes: candidate_count as u64,
5233                budget_bytes: u32::MAX as u64,
5234            });
5235        }
5236
5237        let world_stride =
5238            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
5239        if world_stride == 0 || world_stride > u32::MAX as usize {
5240            return Err(XlogError::ResourceExhausted {
5241                context: "epistemic GPU materialization world stride".to_string(),
5242                estimated_bytes: world_stride as u64,
5243                budget_bytes: u32::MAX as u64,
5244            });
5245        }
5246
5247        let candidate_count = candidate_count as u32;
5248        let world_stride = world_stride as u32;
5249        let func = self
5250            .provider
5251            .device()
5252            .inner()
5253            .get_func(
5254                EPISTEMIC_MODULE,
5255                epistemic_kernels::EPISTEMIC_MATERIALIZE_ACCEPTED_CANDIDATES_U8,
5256            )
5257            .ok_or_else(|| {
5258                XlogError::Execution(
5259                    "epistemic candidate materialization kernel not found".to_string(),
5260                )
5261            })?;
5262        let config = LaunchConfig::for_num_elems(candidate_count);
5263
5264        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
5265            "epistemic GPU candidate materialization",
5266            || unsafe {
5267                // SAFETY: kernel arguments match the PTX signature; the capacity checks
5268                // above prove world-view and rejection buffers cover all accesses.
5269                func.clone().launch(
5270                    config,
5271                    (
5272                        candidate_count,
5273                        world_stride,
5274                        &workspace.rejection_reasons,
5275                        &mut workspace.world_views,
5276                    ),
5277                )
5278            },
5279        )?;
5280
5281        Ok(trace.with_kernel_timing(kernel_timing))
5282    }
5283
5284    /// Materialize final result flags from the reduced runtime output row count.
5285    pub fn materialize_epistemic_gpu_final_results(
5286        &self,
5287        workspace: &mut EpistemicGpuWorkspace,
5288        output: &CudaBuffer,
5289        candidate_count: usize,
5290    ) -> Result<EpistemicGpuFinalResultMaterializationTrace> {
5291        let trace = EpistemicGpuFinalResultMaterializationTrace::for_count(candidate_count)?;
5292        if trace.world_view_slots_written > workspace.layout.world_view_bytes {
5293            return Err(XlogError::ResourceExhausted {
5294                context: "epistemic GPU final-result world-view workspace".to_string(),
5295                estimated_bytes: trace.world_view_slots_written as u64,
5296                budget_bytes: workspace.layout.world_view_bytes as u64,
5297            });
5298        }
5299        if candidate_count > workspace.layout.rejection_reason_slots {
5300            return Err(XlogError::ResourceExhausted {
5301                context: "epistemic GPU final-result rejection workspace".to_string(),
5302                estimated_bytes: candidate_count as u64,
5303                budget_bytes: workspace.layout.rejection_reason_slots as u64,
5304            });
5305        }
5306        if candidate_count > u32::MAX as usize {
5307            return Err(XlogError::ResourceExhausted {
5308                context: "epistemic GPU final-result launch".to_string(),
5309                estimated_bytes: candidate_count as u64,
5310                budget_bytes: u32::MAX as u64,
5311            });
5312        }
5313
5314        let world_stride =
5315            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
5316        if world_stride == 0 || world_stride > u32::MAX as usize {
5317            return Err(XlogError::ResourceExhausted {
5318                context: "epistemic GPU final-result world stride".to_string(),
5319                estimated_bytes: world_stride as u64,
5320                budget_bytes: u32::MAX as u64,
5321            });
5322        }
5323
5324        let candidate_count = candidate_count as u32;
5325        let world_stride = world_stride as u32;
5326        let func = self
5327            .provider
5328            .device()
5329            .inner()
5330            .get_func(
5331                EPISTEMIC_MODULE,
5332                epistemic_kernels::EPISTEMIC_MATERIALIZE_FINAL_RESULT_FLAGS_U8,
5333            )
5334            .ok_or_else(|| {
5335                XlogError::Execution(
5336                    "epistemic final-result materialization kernel not found".to_string(),
5337                )
5338            })?;
5339        let config = LaunchConfig::for_num_elems(candidate_count);
5340
5341        let kernel_timing = self.time_epistemic_gpu_kernel_launch(
5342            "epistemic GPU final result materialization",
5343            || unsafe {
5344                // SAFETY: kernel arguments match the PTX signature; the capacity checks
5345                // above prove world-view and rejection buffers cover all accesses, and
5346                // output.num_rows_device() is the runtime-owned device scalar for output
5347                // row count metadata.
5348                func.clone().launch(
5349                    config,
5350                    (
5351                        candidate_count,
5352                        world_stride,
5353                        output.num_rows_device(),
5354                        &workspace.rejection_reasons,
5355                        &mut workspace.world_views,
5356                    ),
5357                )
5358            },
5359        )?;
5360
5361        Ok(trace.with_kernel_timing(kernel_timing))
5362    }
5363
5364    /// Materialize final query tuples into a device-resident output buffer.
5365    #[allow(clippy::too_many_arguments)]
5366    pub fn materialize_epistemic_gpu_final_tuples(
5367        &self,
5368        workspace: &mut EpistemicGpuWorkspace,
5369        output: &CudaBuffer,
5370        gpu_plan: &EpistemicGpuPlan,
5371        literal_count: usize,
5372        candidate_count: usize,
5373        reduction_count: usize,
5374        models_per_reduction: usize,
5375    ) -> Result<(CudaBuffer, EpistemicGpuFinalTupleMaterializationTrace)> {
5376        self.materialize_epistemic_gpu_final_tuples_scoped(
5377            workspace,
5378            output,
5379            gpu_plan,
5380            literal_count,
5381            candidate_count,
5382            reduction_count,
5383            models_per_reduction,
5384            None,
5385        )
5386    }
5387
5388    /// Materialize final query tuples, optionally scoping the modal row-filter to a
5389    /// single coalesced head's reductions.
5390    ///
5391    /// `head_reduction_filter` is the JOINT-SOLVING multi-output seam: the joint
5392    /// candidate enumeration + world-view validation runs ONCE over the combined
5393    /// modal literals (so the accepted world view in `workspace` is shared by every
5394    /// head), then this method is called once per distinct head with that head's
5395    /// reduction indices. Only modal bindings whose `reduction_index` is in the
5396    /// filter drive that head's row-level or global output gates; the full joint plan
5397    /// (`gpu_plan`) is still validated and the joint workspace dimensions are
5398    /// preserved, so each head is materialized against the SAME accepted world
5399    /// view. `None` materializes against every binding (the single-head path).
5400    #[allow(clippy::too_many_arguments)]
5401    fn materialize_epistemic_gpu_final_tuples_scoped(
5402        &self,
5403        workspace: &mut EpistemicGpuWorkspace,
5404        output: &CudaBuffer,
5405        gpu_plan: &EpistemicGpuPlan,
5406        literal_count: usize,
5407        candidate_count: usize,
5408        reduction_count: usize,
5409        models_per_reduction: usize,
5410        head_reduction_filter: Option<&BTreeSet<usize>>,
5411    ) -> Result<(CudaBuffer, EpistemicGpuFinalTupleMaterializationTrace)> {
5412        gpu_plan.validate_tuple_membership_bindings()?;
5413        if candidate_count > workspace.layout.rejection_reason_slots {
5414            return Err(XlogError::ResourceExhausted {
5415                context: "epistemic GPU final-tuple rejection workspace".to_string(),
5416                estimated_bytes: candidate_count as u64,
5417                budget_bytes: workspace.layout.rejection_reason_slots as u64,
5418            });
5419        }
5420        let literal_count_u32 =
5421            checked_u32_dimension(literal_count, "epistemic GPU final-tuple literals")?;
5422        let candidate_count_u32 =
5423            checked_u32_dimension(candidate_count, "epistemic GPU final-tuple candidates")?;
5424        let reduction_count_u32 =
5425            checked_u32_dimension(reduction_count, "epistemic GPU final-tuple reductions")?;
5426        let models_per_reduction_u32 = checked_u32_dimension(
5427            models_per_reduction,
5428            "epistemic GPU final-tuple models per reduction",
5429        )?;
5430        let output_row_capacity =
5431            usize::try_from(output.num_rows()).map_err(|_| XlogError::ResourceExhausted {
5432                context: "epistemic GPU final-tuple output rows".to_string(),
5433                estimated_bytes: output.num_rows(),
5434                budget_bytes: usize::MAX as u64,
5435            })?;
5436        let output_row_capacity_u32 =
5437            checked_u32_dimension(output_row_capacity, "epistemic GPU final-tuple output rows")?;
5438        let final_output_columns =
5439            final_output_columns_for_materialization(output, gpu_plan, head_reduction_filter)?;
5440        let mut tuple_bytes_capacity = 0usize;
5441        let mut source_columns: Vec<(&CudaColumn, u32, u32)> =
5442            Vec::with_capacity(final_output_columns.len());
5443        let mut result_columns_raw: Vec<TrackedCudaSlice<u8>> =
5444            Vec::with_capacity(final_output_columns.len());
5445        let mut final_schema_columns = Vec::with_capacity(final_output_columns.len());
5446        let mut final_schema_sort_labels = Vec::with_capacity(final_output_columns.len());
5447        for &col_idx in &final_output_columns {
5448            let src_col = output.column(col_idx).ok_or_else(|| {
5449                XlogError::Execution(format!("epistemic final tuple missing column {col_idx}"))
5450            })?;
5451            let (column_name, column_type) = output
5452                .schema()
5453                .columns
5454                .get(col_idx)
5455                .ok_or_else(|| {
5456                    XlogError::Execution(format!(
5457                        "epistemic final tuple missing schema column {col_idx}"
5458                    ))
5459                })?
5460                .clone();
5461            let column_width = column_type.size_bytes();
5462            let expected_column_bytes = checked_product(output_row_capacity, column_width)?;
5463            if src_col.len() < expected_column_bytes {
5464                return Err(XlogError::ResourceExhausted {
5465                    context: "epistemic GPU final-tuple column capacity".to_string(),
5466                    estimated_bytes: expected_column_bytes as u64,
5467                    budget_bytes: src_col.len() as u64,
5468                });
5469            }
5470            let column_byte_len =
5471                checked_u32_dimension(src_col.len(), "epistemic GPU final-tuple column")?;
5472            let column_width =
5473                checked_u32_dimension(column_width, "epistemic GPU final-tuple column width")?;
5474            tuple_bytes_capacity = checked_sum(tuple_bytes_capacity, src_col.len())?;
5475            source_columns.push((src_col, column_byte_len, column_width));
5476            result_columns_raw.push(self.provider.memory().alloc::<u8>(src_col.len())?);
5477            final_schema_columns.push((column_name, column_type));
5478            final_schema_sort_labels.push(
5479                output
5480                    .schema()
5481                    .column_sort_label(col_idx)
5482                    .unwrap_or("")
5483                    .to_string(),
5484            );
5485        }
5486
5487        let mut final_row_count = self.provider.memory().alloc::<u32>(1)?;
5488        let mut row_map = self
5489            .provider
5490            .memory()
5491            .alloc::<u32>(output_row_capacity.max(1))?;
5492        let row_filter_bindings: Vec<_> = gpu_plan
5493            .tuple_membership_bindings
5494            .iter()
5495            .filter(|binding| binding.bound_output_columns.iter().any(Option::is_some))
5496            .filter(|binding| {
5497                head_reduction_filter
5498                    .map(|reductions| reductions.contains(&binding.reduction_index))
5499                    .unwrap_or(true)
5500            })
5501            .collect();
5502        if row_filter_bindings.len() > u32::MAX as usize {
5503            return Err(XlogError::ResourceExhausted {
5504                context: "epistemic GPU final tuple row-filter count".to_string(),
5505                estimated_bytes: row_filter_bindings.len() as u64,
5506                budget_bytes: u32::MAX as u64,
5507            });
5508        }
5509        let negated_row_filter_count = row_filter_bindings
5510            .iter()
5511            .filter(|binding| binding.negated)
5512            .count();
5513        let trace = EpistemicGpuFinalTupleMaterializationTrace::for_counts(
5514            final_output_columns.len(),
5515            output_row_capacity,
5516            tuple_bytes_capacity,
5517            literal_count,
5518            candidate_count,
5519            reduction_count,
5520            models_per_reduction,
5521        )?
5522        .with_row_filter_counts(row_filter_bindings.len(), negated_row_filter_count)?;
5523        if trace.model_membership_bytes_checked > workspace.layout.model_membership_bytes {
5524            return Err(XlogError::ResourceExhausted {
5525                context: "epistemic GPU final-tuple membership workspace".to_string(),
5526                estimated_bytes: trace.model_membership_bytes_checked as u64,
5527                budget_bytes: workspace.layout.model_membership_bytes as u64,
5528            });
5529        }
5530        if trace.world_view_slots_checked > workspace.layout.world_view_bytes {
5531            return Err(XlogError::ResourceExhausted {
5532                context: "epistemic GPU final-tuple world-view workspace".to_string(),
5533                estimated_bytes: trace.world_view_slots_checked as u64,
5534                budget_bytes: workspace.layout.world_view_bytes as u64,
5535            });
5536        }
5537        if trace.model_membership_bytes_checked > u32::MAX as usize {
5538            return Err(XlogError::ResourceExhausted {
5539                context: "epistemic GPU final-tuple membership launch".to_string(),
5540                estimated_bytes: trace.model_membership_bytes_checked as u64,
5541                budget_bytes: u32::MAX as u64,
5542            });
5543        }
5544
5545        let world_stride =
5546            workspace.layout.world_view_bytes / workspace.layout.rejection_reason_slots;
5547        if world_stride == 0 || world_stride > u32::MAX as usize {
5548            return Err(XlogError::ResourceExhausted {
5549                context: "epistemic GPU final-tuple world stride".to_string(),
5550                estimated_bytes: world_stride as u64,
5551                budget_bytes: u32::MAX as u64,
5552            });
5553        }
5554        let world_stride =
5555            checked_u32_dimension(world_stride, "epistemic GPU final-tuple world stride")?;
5556        let mut metadata_len = 0usize;
5557        for binding in &row_filter_bindings {
5558            metadata_len = checked_sum(metadata_len, binding.key_columns.len())?;
5559        }
5560        let metadata_len = metadata_len.max(1);
5561        let row_filter_metadata_len = row_filter_bindings.len().max(1);
5562        checked_u32_dimension(
5563            metadata_len,
5564            "epistemic GPU final tuple row-filter key metadata",
5565        )?;
5566        checked_u32_dimension(
5567            row_filter_metadata_len,
5568            "epistemic GPU final tuple row-filter metadata",
5569        )?;
5570        let memory = self.provider.memory();
5571        let device = self.provider.device().inner();
5572        let mut tuple_source_row_count_ptrs = memory.alloc::<u64>(row_filter_metadata_len)?;
5573        let mut row_filter_negated = memory.alloc::<u8>(row_filter_metadata_len)?;
5574        let mut row_filter_key_offsets = memory.alloc::<u32>(row_filter_metadata_len)?;
5575        let mut row_filter_key_counts = memory.alloc::<u32>(row_filter_metadata_len)?;
5576        let mut key_col_ptrs = memory.alloc::<u64>(metadata_len)?;
5577        let mut key_col_widths = memory.alloc::<u32>(metadata_len)?;
5578        let mut expected_key_bits = memory.alloc::<u64>(metadata_len)?;
5579        let mut expected_key_type_codes = memory.alloc::<u8>(metadata_len)?;
5580        let mut tuple_key_match_modes = memory.alloc::<u8>(metadata_len)?;
5581        let mut bound_value_col_ptrs = memory.alloc::<u64>(metadata_len)?;
5582        let mut bound_value_col_widths = memory.alloc::<u32>(metadata_len)?;
5583        let row_filter_count = checked_u32_dimension(
5584            row_filter_bindings.len(),
5585            "epistemic GPU final tuple row-filter count",
5586        )?;
5587        let mut tuple_source_row_counts = Vec::with_capacity(row_filter_bindings.len());
5588
5589        if !row_filter_bindings.is_empty() {
5590            let mut tuple_source_row_count_ptrs_host =
5591                Vec::with_capacity(row_filter_bindings.len());
5592            let mut row_filter_negated_host = Vec::with_capacity(row_filter_bindings.len());
5593            let mut row_filter_key_offsets_host = Vec::with_capacity(row_filter_bindings.len());
5594            let mut row_filter_key_counts_host = Vec::with_capacity(row_filter_bindings.len());
5595            let mut key_col_ptrs_host = Vec::with_capacity(metadata_len);
5596            let mut key_col_widths_host = Vec::with_capacity(metadata_len);
5597            let mut expected_key_bits_host = Vec::with_capacity(metadata_len);
5598            let mut expected_key_type_codes_host = Vec::with_capacity(metadata_len);
5599            let mut tuple_key_match_modes_host = Vec::with_capacity(metadata_len);
5600            let mut bound_value_col_ptrs_host = Vec::with_capacity(metadata_len);
5601            let mut bound_value_col_widths_host = Vec::with_capacity(metadata_len);
5602
5603            for binding in &row_filter_bindings {
5604                let row_filter_key_offset = checked_u32_dimension(
5605                    key_col_ptrs_host.len(),
5606                    "epistemic GPU final tuple row-filter key offset",
5607                )?;
5608                let row_filter_key_count = checked_u32_dimension(
5609                    binding.key_columns.len(),
5610                    "epistemic GPU final tuple row-filter key arity",
5611                )?;
5612                row_filter_key_offsets_host.push(row_filter_key_offset);
5613                row_filter_key_counts_host.push(row_filter_key_count);
5614                row_filter_negated_host.push(binding.negated as u8);
5615
5616                let source_relation = self
5617                    .resolve_modal_tuple_source(binding.predicate.as_str(), binding.arity)
5618                    .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5619                        construct: "epistemic GPU final tuple row filtering".to_string(),
5620                        context: format!(
5621                            "missing tuple source relation {} (arity {}) for final row filter",
5622                            binding.predicate, binding.arity
5623                        ),
5624                    })?;
5625                let tuple_source_row_count = self.clone_device_row_count(source_relation)?;
5626                tuple_source_row_count_ptrs_host.push(*tuple_source_row_count.device_ptr());
5627                tuple_source_row_counts.push(tuple_source_row_count);
5628
5629                for (term_index, &key_col) in binding.key_columns.iter().enumerate() {
5630                    let key_col_ref = source_relation.column(key_col).ok_or_else(|| {
5631                        XlogError::UnsupportedEpistemicConstruct {
5632                            construct: "epistemic GPU final tuple row filtering".to_string(),
5633                            context: format!(
5634                                "tuple source relation {} missing key column {}",
5635                                binding.predicate, key_col
5636                            ),
5637                        }
5638                    })?;
5639                    let key_col_type =
5640                        source_relation
5641                            .schema()
5642                            .column_type(key_col)
5643                            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5644                                construct: "epistemic GPU final tuple row filtering".to_string(),
5645                                context: format!(
5646                                    "tuple source relation {} missing schema for key column {}",
5647                                    binding.predicate, key_col
5648                                ),
5649                            })?;
5650                    let key_col_width = key_col_type.size_bytes();
5651                    if key_col_width > u32::MAX as usize {
5652                        return Err(XlogError::ResourceExhausted {
5653                            context: "epistemic GPU final tuple row-filter key width".to_string(),
5654                            estimated_bytes: key_col_width as u64,
5655                            budget_bytes: u32::MAX as u64,
5656                        });
5657                    }
5658
5659                    key_col_ptrs_host.push(*key_col_ref.device_ptr());
5660                    key_col_widths_host.push(key_col_width as u32);
5661                    match &binding.key_terms[term_index] {
5662                        EirTerm::Variable(variable_name) => {
5663                            let bound_col_index = binding.bound_output_columns[term_index]
5664                                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5665                                    construct: "epistemic GPU final tuple row filtering"
5666                                        .to_string(),
5667                                    context: format!(
5668                                        "tuple key variable {variable_name} has no reduced \
5669                                             output column binding"
5670                                    ),
5671                                })?;
5672                            let bound_col = output.column(bound_col_index).ok_or_else(|| {
5673                                XlogError::UnsupportedEpistemicConstruct {
5674                                    construct: "epistemic GPU final tuple row filtering"
5675                                        .to_string(),
5676                                    context: format!(
5677                                        "reduced output missing device column {bound_col_index} \
5678                                         for variable {variable_name}"
5679                                    ),
5680                                }
5681                            })?;
5682                            let bound_col_type = output
5683                                .schema()
5684                                .column_type(bound_col_index)
5685                                .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
5686                                    construct: "epistemic GPU final tuple row filtering"
5687                                        .to_string(),
5688                                    context: format!(
5689                                        "reduced output missing schema for variable \
5690                                             {variable_name}"
5691                                    ),
5692                                })?;
5693                            if bound_col_type != key_col_type {
5694                                return Err(XlogError::UnsupportedEpistemicConstruct {
5695                                    construct: "epistemic GPU final tuple row filtering"
5696                                        .to_string(),
5697                                    context: format!(
5698                                        "bound variable {variable_name} has output type \
5699                                         {bound_col_type:?}, but tuple source {} key column {} \
5700                                         has type {key_col_type:?}",
5701                                        binding.predicate, key_col
5702                                    ),
5703                                });
5704                            }
5705                            let bound_col_width = bound_col_type.size_bytes();
5706                            if bound_col_width > u32::MAX as usize {
5707                                return Err(XlogError::ResourceExhausted {
5708                                    context: "epistemic GPU final tuple row-filter bound width"
5709                                        .to_string(),
5710                                    estimated_bytes: bound_col_width as u64,
5711                                    budget_bytes: u32::MAX as u64,
5712                                });
5713                            }
5714                            expected_key_bits_host.push(0);
5715                            expected_key_type_codes_host.push(key_col_type.to_code());
5716                            tuple_key_match_modes_host.push(TUPLE_KEY_MATCH_MODE_BOUND_OUTPUT);
5717                            bound_value_col_ptrs_host.push(*bound_col.device_ptr());
5718                            bound_value_col_widths_host.push(bound_col_width as u32);
5719                        }
5720                        EirTerm::Anonymous => {
5721                            // Wildcard: this tuple-key column imposes no
5722                            // equality requirement when filtering output rows.
5723                            expected_key_bits_host.push(0);
5724                            expected_key_type_codes_host.push(key_col_type.to_code());
5725                            tuple_key_match_modes_host.push(TUPLE_KEY_MATCH_MODE_WILDCARD);
5726                            bound_value_col_ptrs_host.push(0);
5727                            bound_value_col_widths_host.push(0);
5728                        }
5729                        term => {
5730                            let expectation = TupleKeyExpectation::from_term(term, key_col_type)?;
5731                            expected_key_bits_host.push(expectation.bits);
5732                            expected_key_type_codes_host.push(expectation.type_code);
5733                            tuple_key_match_modes_host.push(TUPLE_KEY_MATCH_MODE_GROUND);
5734                            bound_value_col_ptrs_host.push(0);
5735                            bound_value_col_widths_host.push(0);
5736                        }
5737                    }
5738                }
5739            }
5740
5741            let metadata_context = "epistemic GPU final tuple row-filter metadata";
5742            self.provider
5743                .htod_launch_metadata_sync_copy_into(
5744                    &tuple_source_row_count_ptrs_host,
5745                    &mut tuple_source_row_count_ptrs,
5746                )
5747                .map_err(|e| {
5748                    XlogError::execution_ctx(
5749                        metadata_context,
5750                        "upload tuple source row-count pointers",
5751                        &e,
5752                    )
5753                })?;
5754            self.provider
5755                .htod_launch_metadata_sync_copy_into(
5756                    &row_filter_negated_host,
5757                    &mut row_filter_negated,
5758                )
5759                .map_err(|e| {
5760                    XlogError::execution_ctx(metadata_context, "upload row-filter polarity", &e)
5761                })?;
5762            self.provider
5763                .htod_launch_metadata_sync_copy_into(
5764                    &row_filter_key_offsets_host,
5765                    &mut row_filter_key_offsets,
5766                )
5767                .map_err(|e| {
5768                    XlogError::execution_ctx(metadata_context, "upload row-filter key offsets", &e)
5769                })?;
5770            self.provider
5771                .htod_launch_metadata_sync_copy_into(
5772                    &row_filter_key_counts_host,
5773                    &mut row_filter_key_counts,
5774                )
5775                .map_err(|e| {
5776                    XlogError::execution_ctx(metadata_context, "upload row-filter key counts", &e)
5777                })?;
5778            self.provider
5779                .htod_launch_metadata_sync_copy_into(&key_col_ptrs_host, &mut key_col_ptrs)
5780                .map_err(|e| {
5781                    XlogError::execution_ctx(metadata_context, "upload key column pointers", &e)
5782                })?;
5783            self.provider
5784                .htod_launch_metadata_sync_copy_into(&key_col_widths_host, &mut key_col_widths)
5785                .map_err(|e| {
5786                    XlogError::execution_ctx(metadata_context, "upload key column widths", &e)
5787                })?;
5788            self.provider
5789                .htod_launch_metadata_sync_copy_into(
5790                    &expected_key_bits_host,
5791                    &mut expected_key_bits,
5792                )
5793                .map_err(|e| {
5794                    XlogError::execution_ctx(metadata_context, "upload expected key bits", &e)
5795                })?;
5796            self.provider
5797                .htod_launch_metadata_sync_copy_into(
5798                    &expected_key_type_codes_host,
5799                    &mut expected_key_type_codes,
5800                )
5801                .map_err(|e| {
5802                    XlogError::execution_ctx(metadata_context, "upload expected key type codes", &e)
5803                })?;
5804            self.provider
5805                .htod_launch_metadata_sync_copy_into(
5806                    &tuple_key_match_modes_host,
5807                    &mut tuple_key_match_modes,
5808                )
5809                .map_err(|e| {
5810                    XlogError::execution_ctx(metadata_context, "upload tuple key match modes", &e)
5811                })?;
5812            self.provider
5813                .htod_launch_metadata_sync_copy_into(
5814                    &bound_value_col_ptrs_host,
5815                    &mut bound_value_col_ptrs,
5816                )
5817                .map_err(|e| {
5818                    XlogError::execution_ctx(
5819                        metadata_context,
5820                        "upload bound value column pointers",
5821                        &e,
5822                    )
5823                })?;
5824            self.provider
5825                .htod_launch_metadata_sync_copy_into(
5826                    &bound_value_col_widths_host,
5827                    &mut bound_value_col_widths,
5828                )
5829                .map_err(|e| {
5830                    XlogError::execution_ctx(
5831                        metadata_context,
5832                        "upload bound value column widths",
5833                        &e,
5834                    )
5835                })?;
5836        } else {
5837            let metadata_context = "epistemic GPU final tuple row-filter metadata";
5838            device
5839                .memset_zeros(&mut tuple_source_row_count_ptrs)
5840                .map_err(|e| {
5841                    XlogError::execution_ctx(
5842                        metadata_context,
5843                        "tuple source row-count pointer memset",
5844                        &e,
5845                    )
5846                })?;
5847            device.memset_zeros(&mut row_filter_negated).map_err(|e| {
5848                XlogError::execution_ctx(metadata_context, "row-filter polarity memset", &e)
5849            })?;
5850            device
5851                .memset_zeros(&mut row_filter_key_offsets)
5852                .map_err(|e| {
5853                    XlogError::execution_ctx(metadata_context, "row-filter key offset memset", &e)
5854                })?;
5855            device
5856                .memset_zeros(&mut row_filter_key_counts)
5857                .map_err(|e| {
5858                    XlogError::execution_ctx(metadata_context, "row-filter key count memset", &e)
5859                })?;
5860            device.memset_zeros(&mut key_col_ptrs).map_err(|e| {
5861                XlogError::execution_ctx(metadata_context, "key column pointer memset", &e)
5862            })?;
5863            device.memset_zeros(&mut key_col_widths).map_err(|e| {
5864                XlogError::execution_ctx(metadata_context, "key column width memset", &e)
5865            })?;
5866            device.memset_zeros(&mut expected_key_bits).map_err(|e| {
5867                XlogError::execution_ctx(metadata_context, "expected key bits memset", &e)
5868            })?;
5869            device
5870                .memset_zeros(&mut expected_key_type_codes)
5871                .map_err(|e| {
5872                    XlogError::execution_ctx(metadata_context, "expected key type code memset", &e)
5873                })?;
5874            device
5875                .memset_zeros(&mut tuple_key_match_modes)
5876                .map_err(|e| {
5877                    XlogError::execution_ctx(metadata_context, "tuple key match mode memset", &e)
5878                })?;
5879            device
5880                .memset_zeros(&mut bound_value_col_ptrs)
5881                .map_err(|e| {
5882                    XlogError::execution_ctx(
5883                        metadata_context,
5884                        "bound value column pointer memset",
5885                        &e,
5886                    )
5887                })?;
5888            device
5889                .memset_zeros(&mut bound_value_col_widths)
5890                .map_err(|e| {
5891                    XlogError::execution_ctx(
5892                        metadata_context,
5893                        "bound value column width memset",
5894                        &e,
5895                    )
5896                })?;
5897        }
5898
5899        // Global-gate literal mask: a literal that does not bind any reduced
5900        // output column (pure-ground, pure-anonymous, or arity-0) is checked by
5901        // the global membership gate rather than a per-row filter. For those
5902        // literals the body literal must actually hold in the accepted
5903        // candidate's world view; per-row (bound-variable) literals are already
5904        // enforced by the row-filter loop above. The accepted candidate's
5905        // assumption bit already folds in `know`/`possible` modality and
5906        // negation (the validation kernel guarantees assumption == observed for
5907        // accepted candidates), so the gate requires the assumption bit to be
5908        // set for every global-gate literal.
5909        // Constraint literals participate in modal world-view evaluation (model
5910        // membership + assumption-bit pinning) but must NOT gate output rows:
5911        // their pruning is enforced by the separate world-view constraint kernel,
5912        // which rejects candidates whose accepted world view satisfies the
5913        // constraint body. Treating them as required gates would invert the
5914        // semantics (emit rows only when the forbidden body holds), so exclude
5915        // them from the output-gating mask.
5916        let mut is_constraint_literal = vec![false; literal_count.max(1)];
5917        for constraint in &gpu_plan.constraints {
5918            for &literal_index in &constraint.literal_indices {
5919                if literal_index < literal_count {
5920                    is_constraint_literal[literal_index] = true;
5921                }
5922            }
5923        }
5924        let mut gate_literal_required_host = vec![0u8; literal_count.max(1)];
5925        for binding in &gpu_plan.tuple_membership_bindings {
5926            if !binding.bound_output_columns.iter().any(Option::is_some)
5927                && binding.literal_index < literal_count
5928                && !is_constraint_literal[binding.literal_index]
5929                && head_reduction_filter
5930                    .map(|reductions| reductions.contains(&binding.reduction_index))
5931                    .unwrap_or(true)
5932            {
5933                gate_literal_required_host[binding.literal_index] = 1u8;
5934            }
5935        }
5936        // A rule mixing a per-row (bound-variable) modal literal with a global
5937        // gate (pure-ground/anonymous/arity-0) literal is materialized soundly:
5938        // the row-map kernel applies the global-gate `gate_literal_required`
5939        // mask on BOTH the global membership path and the per-row membership
5940        // path, so global-gate literals and per-row bound tuple-key gates
5941        // compose conjunctively. The two gate buffers below are passed to the
5942        // row-map kernel for both paths.
5943        let mut gate_literal_required = memory.alloc::<u8>(literal_count.max(1))?;
5944        self.provider
5945            .htod_launch_metadata_sync_copy_into(
5946                &gate_literal_required_host,
5947                &mut gate_literal_required,
5948            )
5949            .map_err(|e| {
5950                XlogError::execution_ctx(
5951                    "epistemic GPU final tuple gate metadata",
5952                    "upload global-gate literal mask",
5953                    &e,
5954                )
5955            })?;
5956
5957        let row_map_func = self
5958            .provider
5959            .device()
5960            .inner()
5961            .get_func(
5962                EPISTEMIC_MODULE,
5963                epistemic_kernels::EPISTEMIC_BUILD_FINAL_TUPLE_ROW_MAP_U8,
5964            )
5965            .ok_or_else(|| {
5966                XlogError::Execution("epistemic final tuple row-map kernel not found".to_string())
5967            })?;
5968        let close_rejections_func = self
5969            .provider
5970            .device()
5971            .inner()
5972            .get_func(
5973                EPISTEMIC_MODULE,
5974                epistemic_kernels::EPISTEMIC_CLOSE_FINAL_TUPLE_REJECTIONS_U8,
5975            )
5976            .ok_or_else(|| {
5977                XlogError::Execution(
5978                    "epistemic final tuple rejection-close kernel not found".to_string(),
5979                )
5980            })?;
5981        let func = self
5982            .provider
5983            .device()
5984            .inner()
5985            .get_func(
5986                EPISTEMIC_MODULE,
5987                epistemic_kernels::EPISTEMIC_MATERIALIZE_FINAL_TUPLE_COLUMN_U8,
5988            )
5989            .ok_or_else(|| {
5990                XlogError::Execution(
5991                    "epistemic final tuple materialization kernel not found".to_string(),
5992                )
5993            })?;
5994
5995        let mut kernel_timings = Vec::with_capacity(checked_sum(source_columns.len(), 2)?);
5996        let row_map_timing = self.time_epistemic_gpu_kernel_launch(
5997            "epistemic GPU final tuple row map",
5998            || unsafe {
5999                self.provider
6000                    .device()
6001                    .inner()
6002                    .memset_zeros(&mut final_row_count)?;
6003                self.provider.device().inner().memset_zeros(&mut row_map)?;
6004                let mut row_map_params: Vec<*mut c_void> = vec![
6005                    output_row_capacity_u32.as_kernel_param(),
6006                    literal_count_u32.as_kernel_param(),
6007                    candidate_count_u32.as_kernel_param(),
6008                    reduction_count_u32.as_kernel_param(),
6009                    models_per_reduction_u32.as_kernel_param(),
6010                    world_stride.as_kernel_param(),
6011                    output.num_rows_device().as_kernel_param(),
6012                    (&workspace.rejection_reasons).as_kernel_param(),
6013                    (&workspace.model_membership).as_kernel_param(),
6014                    (&workspace.world_views).as_kernel_param(),
6015                    (&tuple_source_row_count_ptrs).as_kernel_param(),
6016                    (&row_filter_negated).as_kernel_param(),
6017                    (&row_filter_key_offsets).as_kernel_param(),
6018                    (&row_filter_key_counts).as_kernel_param(),
6019                    (&key_col_ptrs).as_kernel_param(),
6020                    (&key_col_widths).as_kernel_param(),
6021                    (&expected_key_bits).as_kernel_param(),
6022                    (&expected_key_type_codes).as_kernel_param(),
6023                    (&tuple_key_match_modes).as_kernel_param(),
6024                    (&bound_value_col_ptrs).as_kernel_param(),
6025                    (&bound_value_col_widths).as_kernel_param(),
6026                    row_filter_count.as_kernel_param(),
6027                    (&row_map).as_kernel_param(),
6028                    (&final_row_count).as_kernel_param(),
6029                    (&workspace.candidate_assumptions).as_kernel_param(),
6030                    (&gate_literal_required).as_kernel_param(),
6031                ];
6032                row_map_func.clone().launch(
6033                    LaunchConfig::for_num_elems(output_row_capacity_u32.max(1)),
6034                    &mut row_map_params,
6035                )?;
6036                Ok(())
6037            },
6038        )?;
6039        kernel_timings.push(row_map_timing);
6040
6041        let close_rejections_timing = self.time_epistemic_gpu_kernel_launch(
6042            "epistemic GPU final tuple rejection closeout",
6043            || unsafe {
6044                let mut close_rejections_params: Vec<*mut c_void> = vec![
6045                    candidate_count_u32.as_kernel_param(),
6046                    world_stride.as_kernel_param(),
6047                    (&final_row_count).as_kernel_param(),
6048                    (&workspace.rejection_reasons).as_kernel_param(),
6049                    (&workspace.world_views).as_kernel_param(),
6050                ];
6051                close_rejections_func.clone().launch(
6052                    LaunchConfig::for_num_elems(candidate_count_u32.max(1)),
6053                    &mut close_rejections_params,
6054                )?;
6055                Ok(())
6056            },
6057        )?;
6058        kernel_timings.push(close_rejections_timing);
6059
6060        for ((src_col, column_byte_len, column_row_width), dst_col) in
6061            source_columns.iter().zip(result_columns_raw.iter_mut())
6062        {
6063            let column_timing = self.time_epistemic_gpu_kernel_launch(
6064                "epistemic GPU final tuple column materialization",
6065                || unsafe {
6066                    // SAFETY: source and destination columns are valid device byte
6067                    // buffers of identical length, the row-count scalar and schema
6068                    // row width are runtime-owned, and membership/world-view buffers
6069                    // were capacity-checked.
6070                    let mut params: Vec<*mut c_void> = vec![
6071                        column_byte_len.as_kernel_param(),
6072                        column_row_width.as_kernel_param(),
6073                        literal_count_u32.as_kernel_param(),
6074                        candidate_count_u32.as_kernel_param(),
6075                        reduction_count_u32.as_kernel_param(),
6076                        models_per_reduction_u32.as_kernel_param(),
6077                        world_stride.as_kernel_param(),
6078                        output.num_rows_device().as_kernel_param(),
6079                        (&workspace.rejection_reasons).as_kernel_param(),
6080                        (&workspace.model_membership).as_kernel_param(),
6081                        (&workspace.world_views).as_kernel_param(),
6082                        (&row_map).as_kernel_param(),
6083                        (*src_col).as_kernel_param(),
6084                        dst_col.as_kernel_param(),
6085                        (&final_row_count).as_kernel_param(),
6086                    ];
6087                    func.clone().launch(
6088                        LaunchConfig::for_num_elems((*column_byte_len).max(1)),
6089                        &mut params,
6090                    )?;
6091                    Ok(())
6092                },
6093            )?;
6094            kernel_timings.push(column_timing);
6095        }
6096        let kernel_timing = EpistemicGpuKernelTimingTrace::checked_sum(kernel_timings)?;
6097
6098        let result_columns: Vec<CudaColumn> =
6099            result_columns_raw.into_iter().map(Into::into).collect();
6100        let final_schema = Schema::new(final_schema_columns)
6101            .with_sort_labels(final_schema_sort_labels)
6102            .map_err(|err| XlogError::Execution(format!("epistemic final schema: {err}")))?;
6103        let final_output = CudaBuffer::from_columns(
6104            result_columns,
6105            output.num_rows(),
6106            final_row_count,
6107            final_schema,
6108        );
6109        let final_output = if gpu_plan.final_output_columns.is_none() {
6110            final_output
6111        } else {
6112            self.provider.dedup_full_row(&final_output)?
6113        };
6114
6115        Ok((final_output, trace.with_kernel_timing(kernel_timing)))
6116    }
6117
6118    /// Prepare runtime-owned GPU buffers for an epistemic executable plan.
6119    pub fn prepare_epistemic_gpu_execution(
6120        &self,
6121        executable: &EpistemicExecutablePlan,
6122        capacities: EpistemicGpuWorkspaceCapacities,
6123    ) -> Result<EpistemicGpuPreparedExecution> {
6124        let preflight = EpistemicGpuRuntimePreflight::for_executable_plan(executable, capacities)?;
6125        let mut workspace =
6126            self.allocate_epistemic_gpu_workspace(&executable.gpu_plan, capacities)?;
6127        let workspace_reset = self.reset_epistemic_gpu_workspace(&mut workspace)?;
6128
6129        Ok(EpistemicGpuPreparedExecution {
6130            preflight,
6131            tuple_membership_bindings: executable.gpu_plan.tuple_membership_bindings.clone(),
6132            workspace,
6133            workspace_reset,
6134        })
6135    }
6136
6137    fn validate_epistemic_gpu_reduced_constraints(
6138        &self,
6139        executable: &EpistemicExecutablePlan,
6140    ) -> Result<EpistemicGpuConstraintValidationTrace> {
6141        let mut checked_constraint_relations = 0usize;
6142        let mut violated_constraint_relations = 0usize;
6143        let mut row_count_device_reads = 0u32;
6144        let mut violations = Vec::new();
6145
6146        let mut relation_names = Vec::new();
6147        for rule in executable
6148            .reduced_runtime_plan
6149            .rules_by_scc
6150            .iter()
6151            .flatten()
6152        {
6153            if rule.head.starts_with(XLOG_CONSTRAINT_RELATION_PREFIX)
6154                && !relation_names.iter().any(|name| name == &rule.head)
6155            {
6156                relation_names.push(rule.head.as_str());
6157            }
6158        }
6159
6160        for relation_name in relation_names {
6161            checked_constraint_relations += 1;
6162            let relation = self.store().get(relation_name).ok_or_else(|| {
6163                XlogError::Execution(format!(
6164                    "missing reduced constraint relation {relation_name} after production runtime \
6165                     dispatch"
6166                ))
6167            })?;
6168            let row_count_was_cached = relation.cached_row_count().is_some();
6169            let rows = self.provider.device_row_count(relation)?;
6170            row_count_device_reads += u32::from(!row_count_was_cached);
6171            if rows > 0 {
6172                violated_constraint_relations += 1;
6173                let constraint_index = relation_name
6174                    .strip_prefix(XLOG_CONSTRAINT_RELATION_PREFIX)
6175                    .and_then(|suffix| suffix.parse::<usize>().ok())
6176                    .ok_or_else(|| {
6177                        XlogError::Execution(format!(
6178                            "invalid compiler-generated constraint relation {relation_name}"
6179                        ))
6180                    })?;
6181                violations.push((constraint_index, relation_name.to_string(), rows));
6182            }
6183        }
6184
6185        if let Some((constraint_index, relation_name, witness_rows)) =
6186            violations.into_iter().min_by_key(|(index, _, _)| *index)
6187        {
6188            return Err(XlogError::ConstraintViolation {
6189                constraint_index,
6190                relation_name,
6191                witness_rows,
6192            });
6193        }
6194
6195        Ok(EpistemicGpuConstraintValidationTrace {
6196            checked_constraint_relations,
6197            violated_constraint_relations,
6198            row_count_device_reads,
6199        })
6200    }
6201
6202    /// Materialize a stratum's GATED epistemic head output into the relation store
6203    /// as a base relation, for stratified epistemic execution.
6204    ///
6205    /// After a lower stratum computes its modal-gated head extension (the
6206    /// `final_output`/additional-head buffer), the higher stratum's `know`/
6207    /// `possible` over that head must read the GATED extension — not the ungated
6208    /// reduced relation the reduced runtime plan leaves in the store. This OVERWRITES
6209    /// the store relation under `name` with a device-side clone of the gated buffer,
6210    /// so the existing tuple-membership filter (which reads the source
6211    /// relation from the store by predicate name) gates the higher stratum against
6212    /// the correct extension. No resolve-into-body is performed, so there is no
6213    /// double-gating against the GPU world-view filter.
6214    pub fn materialize_epistemic_head_relation(
6215        &mut self,
6216        name: &str,
6217        gated_output: &CudaBuffer,
6218    ) -> Result<()> {
6219        let cloned = self.clone_buffer(gated_output)?;
6220        self.put_relation(name, cloned);
6221        Ok(())
6222    }
6223
6224    /// Device-side clone of a store-resident relation buffer, for surfacing a
6225    /// stratified ordinary stratum's output as a query result without moving it out
6226    /// of the store.
6227    pub fn clone_store_relation(&self, buffer: &CudaBuffer) -> Result<CudaBuffer> {
6228        self.clone_buffer(buffer)
6229    }
6230
6231    /// Execute the reduced production runtime plan and capture epistemic GPU evidence.
6232    pub fn execute_epistemic_gpu_execution(
6233        &mut self,
6234        executable: &EpistemicExecutablePlan,
6235        capacities: EpistemicGpuWorkspaceCapacities,
6236    ) -> Result<EpistemicGpuExecutionResult> {
6237        let mut prepared = self.prepare_epistemic_gpu_execution(executable, capacities)?;
6238        let literal_count = executable.gpu_plan.epistemic_literals.len();
6239        let candidate_count = bounded_candidate_count(literal_count, capacities.max_candidates)?;
6240        let transfer_budget_start = self.provider.host_transfer_stats();
6241        let launch_metadata_transfer_start = self.provider.host_launch_metadata_transfer_stats();
6242        let candidate_generation = self.generate_epistemic_gpu_candidates(
6243            &mut prepared.workspace,
6244            literal_count,
6245            candidate_count,
6246        )?;
6247        let propagation = self.propagate_epistemic_gpu_candidates(
6248            &mut prepared.workspace,
6249            literal_count,
6250            candidate_count,
6251        )?;
6252        let candidate_validation = self.validate_epistemic_gpu_candidates(
6253            &mut prepared.workspace,
6254            literal_count,
6255            candidate_count,
6256        )?;
6257        let counters_before = self.epistemic_gpu_runtime_counters();
6258        let _reduced_return = self.execute_plan(&executable.reduced_runtime_plan)?;
6259        let counters_after = self.epistemic_gpu_runtime_counters();
6260        let trace = EpistemicGpuRuntimeTrace::try_from_preflight_and_counters(
6261            prepared.preflight,
6262            counters_before,
6263            counters_after,
6264        )?;
6265        trace.require_wcoj_certification()?;
6266        let output_relation = executable
6267            .gpu_plan
6268            .reductions
6269            .last()
6270            .ok_or_else(|| XlogError::UnsupportedEpistemicConstruct {
6271                construct: "epistemic GPU reduced output".to_string(),
6272                context: "executable plan has no epistemic reductions".to_string(),
6273            })?
6274            .head_predicate
6275            .as_str();
6276        let output = {
6277            let reduced_output = self.store().get(output_relation).ok_or_else(|| {
6278                XlogError::UnsupportedEpistemicConstruct {
6279                    construct: "epistemic GPU reduced output".to_string(),
6280                    context: format!(
6281                        "missing reduced output relation {output_relation} after production \
6282                         runtime dispatch"
6283                    ),
6284                }
6285            })?;
6286            self.clone_buffer(reduced_output)?
6287        };
6288        let model_membership = self.populate_epistemic_gpu_model_membership_from_tuple_sources(
6289            &mut prepared.workspace,
6290            &output,
6291            &executable.gpu_plan,
6292            candidate_count,
6293            capacities.max_models_per_reduction,
6294        )?;
6295        model_membership.require_stable_model_tuple_source()?;
6296        let expected_tuple_key_column_reads =
6297            expected_tuple_key_column_reads(&executable.gpu_plan.tuple_membership_bindings)?;
6298        model_membership.require_planned_tuple_key_column_reads(expected_tuple_key_column_reads)?;
6299        let world_view_validation = self.validate_epistemic_gpu_world_views(
6300            &mut prepared.workspace,
6301            &executable.gpu_plan,
6302            candidate_count,
6303            capacities.max_models_per_reduction,
6304        )?;
6305        let constraint_world_view_validation = self.validate_epistemic_gpu_world_view_constraints(
6306            &mut prepared.workspace,
6307            &executable.gpu_plan,
6308            candidate_count,
6309        )?;
6310        let materialization =
6311            self.materialize_epistemic_gpu_candidates(&mut prepared.workspace, candidate_count)?;
6312        let final_result_materialization = self.materialize_epistemic_gpu_final_results(
6313            &mut prepared.workspace,
6314            &output,
6315            candidate_count,
6316        )?;
6317        // Distinct epistemic output heads and the reduction indices that feed each.
6318        // A single-head plan keeps the unscoped (None) row filter. A JOINT-SOLVED
6319        // coalesced multi-head plan materializes EACH head against the SAME accepted
6320        // world view by scoping the modal row-filter to that head's reductions.
6321        let head_reductions = epistemic_head_reduction_indices(&executable.gpu_plan);
6322        let is_multi_head = head_reductions.len() > 1;
6323        let primary_head_filter = if is_multi_head {
6324            head_reductions.get(output_relation).cloned()
6325        } else {
6326            None
6327        };
6328        let (final_output, final_tuple_materialization) = self
6329            .materialize_epistemic_gpu_final_tuples_scoped(
6330                &mut prepared.workspace,
6331                &output,
6332                &executable.gpu_plan,
6333                literal_count,
6334                candidate_count,
6335                executable.gpu_plan.reductions.len(),
6336                capacities.max_models_per_reduction,
6337                primary_head_filter.as_ref(),
6338            )?;
6339        // Materialize every OTHER coupled head against the shared accepted world
6340        // view. Each head's reduced relation (already computed jointly by the single
6341        // reduced-program dispatch above) is the materialization source; only that
6342        // head's modal row-filter bindings apply.
6343        let mut additional_head_outputs: Vec<(String, CudaBuffer)> = Vec::new();
6344        if is_multi_head {
6345            for (head, reductions) in &head_reductions {
6346                if head.as_str() == output_relation {
6347                    continue;
6348                }
6349                let head_output = {
6350                    let reduced_head = self.store().get(head.as_str()).ok_or_else(|| {
6351                        XlogError::UnsupportedEpistemicConstruct {
6352                            construct: "epistemic GPU reduced output".to_string(),
6353                            context: format!(
6354                                "missing reduced output relation {head} after production runtime \
6355                                 dispatch for joint multi-head materialization"
6356                            ),
6357                        }
6358                    })?;
6359                    self.clone_buffer(reduced_head)?
6360                };
6361                let (head_final_output, _head_trace) = self
6362                    .materialize_epistemic_gpu_final_tuples_scoped(
6363                        &mut prepared.workspace,
6364                        &head_output,
6365                        &executable.gpu_plan,
6366                        literal_count,
6367                        candidate_count,
6368                        executable.gpu_plan.reductions.len(),
6369                        capacities.max_models_per_reduction,
6370                        Some(reductions),
6371                    )?;
6372                additional_head_outputs.push((head.clone(), head_final_output));
6373            }
6374        }
6375        let tuple_evidence_output = if executable.gpu_plan.final_output_columns.is_some() {
6376            let mut evidence_plan = executable.gpu_plan.clone();
6377            evidence_plan.final_output_columns = None;
6378            let (evidence_output, _) = self.materialize_epistemic_gpu_final_tuples(
6379                &mut prepared.workspace,
6380                &output,
6381                &evidence_plan,
6382                literal_count,
6383                candidate_count,
6384                executable.gpu_plan.reductions.len(),
6385                capacities.max_models_per_reduction,
6386            )?;
6387            Some(evidence_output)
6388        } else {
6389            None
6390        };
6391        let transfer_budget_end = self.provider.host_transfer_stats();
6392        let launch_metadata_transfer_end = self.provider.host_launch_metadata_transfer_stats();
6393        let constraint_validation = self.validate_epistemic_gpu_reduced_constraints(executable)?;
6394        let transfer_budget =
6395            EpistemicGpuTransferBudgetTrace::from_host_transfer_stats_with_launch_metadata(
6396                candidate_count,
6397                transfer_budget_start,
6398                transfer_budget_end,
6399                launch_metadata_transfer_start,
6400                launch_metadata_transfer_end,
6401            )?;
6402        let final_result_transfer =
6403            EpistemicGpuFinalResultTransferTrace::from_final_output(&self.provider, &final_output)?;
6404        final_tuple_materialization.require_row_filter_materialization_evidence(
6405            "epistemic GPU final tuple materialization",
6406            final_result_transfer.final_output_rows,
6407        )?;
6408        let semantic_trace = EpistemicGpuSemanticTrace::from_device_rejection_reasons(
6409            &self.provider,
6410            &prepared.workspace,
6411            &candidate_generation,
6412            &propagation,
6413            &model_membership,
6414            &world_view_validation,
6415        )?;
6416
6417        Ok(EpistemicGpuExecutionResult {
6418            provider_identity: EpistemicGpuProviderIdentity::from_provider(&self.provider),
6419            prepared,
6420            candidate_generation,
6421            propagation,
6422            candidate_validation,
6423            model_membership,
6424            world_view_validation,
6425            constraint_world_view_validation,
6426            materialization,
6427            final_result_materialization,
6428            final_tuple_materialization,
6429            transfer_budget,
6430            final_result_transfer,
6431            constraint_validation,
6432            semantic_trace,
6433            tuple_membership_bindings: executable.gpu_plan.tuple_membership_bindings.clone(),
6434            final_output,
6435            additional_head_outputs,
6436            tuple_evidence_output,
6437            output,
6438            trace,
6439        })
6440    }
6441
6442    /// Execute multiple accepted epistemic GPU executable plans in order.
6443    ///
6444    /// This is the runtime adapter used by split execution evidence: each
6445    /// component is still dispatched through [`Self::execute_epistemic_gpu_execution`],
6446    /// so candidate generation, model-membership, world-view validation,
6447    /// materialization, transfer-budget, and production runtime counters are
6448    /// recorded by the existing single-plan path.
6449    pub fn execute_epistemic_gpu_execution_batch(
6450        &mut self,
6451        executables: &[&EpistemicExecutablePlan],
6452        capacities: EpistemicGpuWorkspaceCapacities,
6453    ) -> Result<Vec<EpistemicGpuExecutionResult>> {
6454        if executables.is_empty() {
6455            return Err(XlogError::UnsupportedEpistemicConstruct {
6456                construct: "epistemic GPU batch execution".to_string(),
6457                context: "batch execution requires at least one executable component".to_string(),
6458            });
6459        }
6460
6461        let mut results = Vec::with_capacity(executables.len());
6462        for executable in executables {
6463            results.push(self.execute_epistemic_gpu_execution(executable, capacities)?);
6464        }
6465        Ok(results)
6466    }
6467
6468    /// Execute multiple epistemic GPU executable plans and return an aggregate trace.
6469    ///
6470    /// This is used by split-execution certification: every component still
6471    /// routes through the existing single-plan GPU runtime path, and the batch
6472    /// trace only aggregates those component traces. It does not perform CPU
6473    /// recomposition.
6474    pub fn execute_epistemic_gpu_execution_batch_with_trace(
6475        &mut self,
6476        executables: &[&EpistemicExecutablePlan],
6477        capacities: EpistemicGpuWorkspaceCapacities,
6478    ) -> Result<EpistemicGpuBatchExecutionResult> {
6479        let results = self.execute_epistemic_gpu_execution_batch(executables, capacities)?;
6480        let trace = EpistemicGpuBatchExecutionTrace::try_from_component_results(&results)?;
6481        Ok(EpistemicGpuBatchExecutionResult { results, trace })
6482    }
6483}
6484
6485#[derive(Default)]
6486struct RuntimeRouteSummary {
6487    multiway_reduction_count: usize,
6488    kclique_wcoj_plan_count: usize,
6489    wcoj_triangle_route_count: usize,
6490    wcoj_4cycle_route_count: usize,
6491    free_join_route_count: usize,
6492    kclique_wcoj_plan_count_by_arity: [usize; 4],
6493    kclique_wcoj_max_arity: u8,
6494    kclique_wcoj_edge_permutation_count: usize,
6495    kclique_stream_groups: BTreeSet<StreamGroupId>,
6496    kclique_skew_scheduled_plan_count: usize,
6497    planned_hash_route_count: usize,
6498    planned_hash_planner_wins_count: usize,
6499    planned_hash_incomplete_stats_count: usize,
6500    planned_hash_cost_evidence_count: usize,
6501    sorted_layout_requirement_count: usize,
6502    helper_split_spec_count: usize,
6503}
6504
6505fn summarize_runtime_routes(node: &RirNode, routes: &mut RuntimeRouteSummary) {
6506    match node {
6507        RirNode::MultiWayJoin { inputs, plan, .. } => {
6508            routes.multiway_reduction_count += 1;
6509            match plan {
6510                Some(MultiwayPlan::WcojWithPlan(order)) => {
6511                    routes.kclique_wcoj_plan_count += 1;
6512                    if let Some(slot) = usize::from(order.k).checked_sub(5) {
6513                        if slot < routes.kclique_wcoj_plan_count_by_arity.len() {
6514                            routes.kclique_wcoj_plan_count_by_arity[slot] += 1;
6515                        }
6516                    }
6517                    routes.kclique_wcoj_max_arity = routes.kclique_wcoj_max_arity.max(order.k);
6518                    routes.kclique_wcoj_edge_permutation_count += order
6519                        .edge_permutation
6520                        .iter()
6521                        .take_while(|slot| **slot != u8::MAX)
6522                        .count();
6523                    routes.kclique_stream_groups.insert(order.stream_group);
6524                    if !order.helper_split_specs.is_empty() {
6525                        routes.kclique_skew_scheduled_plan_count += 1;
6526                    }
6527                    routes.sorted_layout_requirement_count +=
6528                        order.sorted_layout_requirements.edge_slots.len();
6529                    routes.helper_split_spec_count += order.helper_split_specs.len();
6530                }
6531                Some(MultiwayPlan::PlannedHashRoute {
6532                    reason,
6533                    planner_evidence,
6534                }) => {
6535                    routes.planned_hash_route_count += 1;
6536                    match reason {
6537                        PlannedHashReason::PlannerPredictsHashWins => {
6538                            routes.planned_hash_planner_wins_count += 1;
6539                            if planner_evidence.wcoj_cost.is_finite()
6540                                && planner_evidence.hash_cost.is_finite()
6541                                && planner_evidence.hash_cost <= planner_evidence.wcoj_cost
6542                            {
6543                                routes.planned_hash_cost_evidence_count += 1;
6544                            }
6545                        }
6546                        PlannedHashReason::IncompleteStatsSafeDefault => {
6547                            routes.planned_hash_incomplete_stats_count += 1;
6548                        }
6549                    }
6550                }
6551                // Generic Free Join route: provenance variant set
6552                // only by the general multiway promoter. Opportunistic
6553                // — the dispatcher's structural decline executes the
6554                // embedded binary fallback, so no hard dispatch
6555                // obligation accrues here.
6556                Some(MultiwayPlan::FreeJoin) => {
6557                    routes.free_join_route_count += 1;
6558                }
6559                None => {
6560                    if super::wcoj_dispatch::match_multiway_triangle(node).is_some() {
6561                        routes.wcoj_triangle_route_count += 1;
6562                    } else if super::wcoj_dispatch::match_multiway_4cycle(node).is_some() {
6563                        routes.wcoj_4cycle_route_count += 1;
6564                    }
6565                }
6566            }
6567
6568            for input in inputs {
6569                summarize_runtime_routes(input, routes);
6570            }
6571        }
6572        RirNode::Filter { input, .. }
6573        | RirNode::Project { input, .. }
6574        | RirNode::Distinct { input, .. }
6575        | RirNode::GroupBy { input, .. } => summarize_runtime_routes(input, routes),
6576        RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
6577            summarize_runtime_routes(left, routes);
6578            summarize_runtime_routes(right, routes);
6579        }
6580        RirNode::Union { inputs } => {
6581            for input in inputs {
6582                summarize_runtime_routes(input, routes);
6583            }
6584        }
6585        RirNode::Fixpoint {
6586            base, recursive, ..
6587        } => {
6588            summarize_runtime_routes(base, routes);
6589            summarize_runtime_routes(recursive, routes);
6590        }
6591        RirNode::ChainJoin { left, right, .. } => {
6592            summarize_runtime_routes(left, routes);
6593            summarize_runtime_routes(right, routes);
6594        }
6595        RirNode::TensorMaskedJoin { .. } | RirNode::Scan { .. } | RirNode::Unit => {}
6596    }
6597}
6598
6599fn helper_relation_ids(executable: &EpistemicExecutablePlan) -> BTreeSet<RelId> {
6600    executable
6601        .relation_ids
6602        .iter()
6603        .filter_map(|(name, rel)| name.starts_with("__kclique_helper_").then_some(*rel))
6604        .collect()
6605}
6606
6607fn count_helper_relation_scans(node: &RirNode, helper_relations: &BTreeSet<RelId>) -> usize {
6608    match node {
6609        RirNode::Scan { .. } => 0,
6610        RirNode::MultiWayJoin { plan, inputs, .. } => {
6611            let own_wcoj_inputs = if matches!(plan, Some(MultiwayPlan::WcojWithPlan(_))) {
6612                inputs
6613                    .iter()
6614                    .map(|input| count_helper_relation_leaf_scans(input, helper_relations))
6615                    .sum()
6616            } else {
6617                0
6618            };
6619            own_wcoj_inputs
6620                + inputs
6621                    .iter()
6622                    .map(|input| count_helper_relation_scans(input, helper_relations))
6623                    .sum::<usize>()
6624        }
6625        RirNode::Filter { input, .. }
6626        | RirNode::Project { input, .. }
6627        | RirNode::Distinct { input, .. }
6628        | RirNode::GroupBy { input, .. } => count_helper_relation_scans(input, helper_relations),
6629        RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
6630            count_helper_relation_scans(left, helper_relations)
6631                + count_helper_relation_scans(right, helper_relations)
6632        }
6633        RirNode::Union { inputs } => inputs
6634            .iter()
6635            .map(|input| count_helper_relation_scans(input, helper_relations))
6636            .sum(),
6637        RirNode::Fixpoint {
6638            base, recursive, ..
6639        } => {
6640            count_helper_relation_scans(base, helper_relations)
6641                + count_helper_relation_scans(recursive, helper_relations)
6642        }
6643        RirNode::ChainJoin { left, right, .. } => {
6644            count_helper_relation_scans(left, helper_relations)
6645                + count_helper_relation_scans(right, helper_relations)
6646        }
6647        RirNode::TensorMaskedJoin { .. } | RirNode::Unit => 0,
6648    }
6649}
6650
6651fn count_helper_relation_leaf_scans(node: &RirNode, helper_relations: &BTreeSet<RelId>) -> usize {
6652    match node {
6653        RirNode::Scan { rel } => usize::from(helper_relations.contains(rel)),
6654        RirNode::Filter { input, .. }
6655        | RirNode::Project { input, .. }
6656        | RirNode::Distinct { input, .. }
6657        | RirNode::GroupBy { input, .. } => {
6658            count_helper_relation_leaf_scans(input, helper_relations)
6659        }
6660        RirNode::Join { left, right, .. } | RirNode::Diff { left, right } => {
6661            count_helper_relation_leaf_scans(left, helper_relations)
6662                + count_helper_relation_leaf_scans(right, helper_relations)
6663        }
6664        RirNode::Union { inputs } => inputs
6665            .iter()
6666            .map(|input| count_helper_relation_leaf_scans(input, helper_relations))
6667            .sum(),
6668        RirNode::Fixpoint {
6669            base, recursive, ..
6670        } => {
6671            count_helper_relation_leaf_scans(base, helper_relations)
6672                + count_helper_relation_leaf_scans(recursive, helper_relations)
6673        }
6674        RirNode::MultiWayJoin { inputs, .. } => inputs
6675            .iter()
6676            .map(|input| count_helper_relation_leaf_scans(input, helper_relations))
6677            .sum(),
6678        RirNode::ChainJoin { left, right, .. } => {
6679            count_helper_relation_leaf_scans(left, helper_relations)
6680                + count_helper_relation_leaf_scans(right, helper_relations)
6681        }
6682        RirNode::TensorMaskedJoin { .. } | RirNode::Unit => 0,
6683    }
6684}
6685
6686fn require_positive(value: usize, context: &str) -> Result<()> {
6687    if value == 0 {
6688        return Err(XlogError::ResourceExhausted {
6689            context: context.to_string(),
6690            estimated_bytes: 0,
6691            budget_bytes: 1,
6692        });
6693    }
6694    Ok(())
6695}
6696
6697fn checked_u32_dimension(value: usize, context: &str) -> Result<u32> {
6698    u32::try_from(value).map_err(|_| XlogError::ResourceExhausted {
6699        context: context.to_string(),
6700        estimated_bytes: value as u64,
6701        budget_bytes: u32::MAX as u64,
6702    })
6703}
6704
6705/// Map each distinct epistemic output head to the reduction indices feeding it.
6706///
6707/// Reduction index = position in `gpu_plan.reductions`, which is exactly the
6708/// `reduction_index` carried by every tuple-membership binding, so the returned
6709/// sets scope each head's modal row-filter for joint multi-head materialization.
6710fn epistemic_head_reduction_indices(
6711    gpu_plan: &EpistemicGpuPlan,
6712) -> std::collections::BTreeMap<String, BTreeSet<usize>> {
6713    let mut heads: std::collections::BTreeMap<String, BTreeSet<usize>> =
6714        std::collections::BTreeMap::new();
6715    for (reduction_index, reduction) in gpu_plan.reductions.iter().enumerate() {
6716        heads
6717            .entry(reduction.head_predicate.clone())
6718            .or_default()
6719            .insert(reduction_index);
6720    }
6721    heads
6722}
6723
6724fn final_output_columns_for_materialization(
6725    output: &CudaBuffer,
6726    gpu_plan: &EpistemicGpuPlan,
6727    head_reduction_filter: Option<&BTreeSet<usize>>,
6728) -> Result<Vec<usize>> {
6729    // PER-HEAD augmented projection: in a JOINT-SOLVED multi-head component each head
6730    // is materialized from its OWN reduced relation buffer with its OWN reduction
6731    // filter. The plan-global `final_output_columns` is derived from the first
6732    // epistemic rule's head and would mis-project coupled heads of DIFFERING arity.
6733    // When a head filter is supplied, project the first `public_head_arity` columns of
6734    // THAT head (the augmented modal-literal columns are appended after the public head
6735    // terms), so heads of differing arity/projection each materialize their own public
6736    // tuple shape. This reads only the store/world-view boundary (the reduced relation
6737    // buffer's arity + the plan's recorded public arity) — never a resolved body.
6738    if let Some(filter) = head_reduction_filter {
6739        if let Some(public_head_arity) = gpu_plan
6740            .reductions
6741            .iter()
6742            .enumerate()
6743            .filter(|(reduction_index, _)| filter.contains(reduction_index))
6744            .map(|(_, reduction)| reduction.public_head_arity)
6745            .max()
6746        {
6747            if public_head_arity > output.arity() {
6748                return Err(XlogError::UnsupportedEpistemicConstruct {
6749                    construct: "epistemic GPU final output projection".to_string(),
6750                    context: format!(
6751                        "per-head public arity {} exceeds reduced output arity {} for the \
6752                         joint multi-head materialization",
6753                        public_head_arity,
6754                        output.arity()
6755                    ),
6756                });
6757            }
6758            return Ok((0..public_head_arity).collect());
6759        }
6760    }
6761
6762    let Some(final_output_columns) = &gpu_plan.final_output_columns else {
6763        return Ok((0..output.arity()).collect());
6764    };
6765
6766    let mut seen = vec![false; output.arity()];
6767    for &column in final_output_columns {
6768        if column >= output.arity() {
6769            return Err(XlogError::UnsupportedEpistemicConstruct {
6770                construct: "epistemic GPU final output projection".to_string(),
6771                context: format!(
6772                    "final output column {} exceeds reduced output arity {}",
6773                    column,
6774                    output.arity()
6775                ),
6776            });
6777        }
6778        if seen[column] {
6779            return Err(XlogError::UnsupportedEpistemicConstruct {
6780                construct: "epistemic GPU final output projection".to_string(),
6781                context: format!("duplicate final output column {}", column),
6782            });
6783        }
6784        seen[column] = true;
6785    }
6786
6787    Ok(final_output_columns.clone())
6788}
6789
6790fn require_u32_launch_bound(value: usize, context: &str) -> Result<()> {
6791    checked_u32_dimension(value, context).map(|_| ())
6792}
6793
6794fn require_u32_launch_dimensions(values: &[usize], context: &str) -> Result<()> {
6795    let max_value = values.iter().copied().max().unwrap_or(0);
6796    require_u32_launch_bound(max_value, context)
6797}
6798
6799fn checked_product(left: usize, right: usize) -> Result<usize> {
6800    left.checked_mul(right).ok_or_else(|| {
6801        XlogError::Kernel(format!(
6802            "epistemic GPU workspace size overflow: {left} * {right}"
6803        ))
6804    })
6805}
6806
6807fn checked_sum(left: usize, right: usize) -> Result<usize> {
6808    left.checked_add(right).ok_or_else(|| {
6809        XlogError::Kernel(format!(
6810            "epistemic GPU workspace size overflow: {left} + {right}"
6811        ))
6812    })
6813}
6814
6815fn require_epistemic_gpu_kernel_phases(gpu_plan: &EpistemicGpuPlan) -> Result<()> {
6816    let required = [
6817        EpistemicGpuHotPathPhase::CandidateGeneration,
6818        EpistemicGpuHotPathPhase::Propagation,
6819        EpistemicGpuHotPathPhase::CandidateValidation,
6820        EpistemicGpuHotPathPhase::ModelMembership,
6821        EpistemicGpuHotPathPhase::WorldViewValidation,
6822        EpistemicGpuHotPathPhase::ResultMaterialization,
6823        EpistemicGpuHotPathPhase::FinalResultMaterialization,
6824        EpistemicGpuHotPathPhase::FinalTupleMaterialization,
6825    ];
6826
6827    for phase in required {
6828        if !gpu_plan.required_kernel_phases.contains(&phase) {
6829            return Err(XlogError::UnsupportedEpistemicConstruct {
6830                construct: "epistemic GPU kernel phase contract".to_string(),
6831                context: format!(
6832                    "accepted GPU execution requires kernel phase {:?}, but the plan declared {:?}",
6833                    phase, gpu_plan.required_kernel_phases
6834                ),
6835            });
6836        }
6837    }
6838
6839    Ok(())
6840}
6841
6842fn require_epistemic_gpu_buffer_contract(gpu_plan: &EpistemicGpuPlan) -> Result<()> {
6843    let required = [
6844        EpistemicGpuBufferKind::CandidateAssumptions,
6845        EpistemicGpuBufferKind::WorldViews,
6846        EpistemicGpuBufferKind::ModelMembership,
6847        EpistemicGpuBufferKind::RejectionReasons,
6848    ];
6849
6850    for buffer in required {
6851        if !gpu_plan.required_buffers.contains(&buffer) {
6852            return Err(XlogError::UnsupportedEpistemicConstruct {
6853                construct: "epistemic GPU buffer contract".to_string(),
6854                context: format!(
6855                    "accepted GPU execution requires buffer {:?}, but the plan declared {:?}",
6856                    buffer, gpu_plan.required_buffers
6857                ),
6858            });
6859        }
6860    }
6861
6862    Ok(())
6863}
6864
6865fn expected_tuple_key_column_reads(bindings: &[EpistemicTupleMembershipBinding]) -> Result<usize> {
6866    bindings.iter().try_fold(0usize, |acc, binding| {
6867        checked_sum(acc, binding.key_columns.len())
6868    })
6869}
6870
6871fn world_view_bitset_bytes_per_candidate(literal_count: usize) -> Result<usize> {
6872    Ok(checked_sum(literal_count, 7)? / 8)
6873}
6874
6875fn epistemic_operator_code(op: EirEpistemicOp) -> u8 {
6876    match op {
6877        EirEpistemicOp::Know => 1,
6878        EirEpistemicOp::Possible => 2,
6879    }
6880}
6881
6882fn bounded_candidate_count(literal_count: usize, max_candidates: usize) -> Result<usize> {
6883    require_positive(literal_count, "epistemic GPU execution literals")?;
6884    require_positive(max_candidates, "epistemic GPU execution candidates")?;
6885    if literal_count > 31 {
6886        return Err(XlogError::UnsupportedEpistemicConstruct {
6887            construct: "epistemic GPU execution candidate generation".to_string(),
6888            context: format!("literal count {literal_count} exceeds 31-bit candidate mask"),
6889        });
6890    }
6891    let required_candidates = 1usize << literal_count;
6892    if max_candidates < required_candidates {
6893        return Err(XlogError::ResourceExhausted {
6894            context: "epistemic GPU execution candidate capacity".to_string(),
6895            estimated_bytes: required_candidates as u64,
6896            budget_bytes: max_candidates as u64,
6897        });
6898    }
6899    Ok(required_candidates)
6900}
6901
6902#[cfg(test)]
6903mod tests {
6904    use super::*;
6905    use xlog_core::ScalarType;
6906    use xlog_ir::EirTerm;
6907
6908    #[test]
6909    fn tuple_key_expectation_encodes_ground_integer_for_u32_column() {
6910        let expectation =
6911            TupleKeyExpectation::from_term(&EirTerm::Integer(42), ScalarType::U32).unwrap();
6912
6913        assert_eq!(
6914            expectation,
6915            TupleKeyExpectation {
6916                bits: 42,
6917                type_code: ScalarType::U32.to_code(),
6918            }
6919        );
6920    }
6921
6922    #[test]
6923    fn tuple_key_expectation_encodes_symbol_for_symbol_column() {
6924        let expectation =
6925            TupleKeyExpectation::from_term(&EirTerm::Symbol(7), ScalarType::Symbol).unwrap();
6926
6927        assert_eq!(
6928            expectation,
6929            TupleKeyExpectation {
6930                bits: 7,
6931                type_code: ScalarType::Symbol.to_code(),
6932            }
6933        );
6934    }
6935
6936    #[test]
6937    fn tuple_key_expectation_rejects_variable_as_ground_expectation() {
6938        let err =
6939            TupleKeyExpectation::from_term(&EirTerm::Variable("X".to_string()), ScalarType::U32)
6940                .expect_err("variable tuple keys require bound-output matching");
6941
6942        match err {
6943            XlogError::UnsupportedEpistemicConstruct { construct, context } => {
6944                assert_eq!(construct, "epistemic GPU tuple-key expectation");
6945                assert!(context.contains("cannot be encoded as a ground tuple-key expectation"));
6946            }
6947            other => panic!("expected tuple-key expectation error, got {other:?}"),
6948        }
6949    }
6950}