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