Skip to content

Commit a64fb70

Browse files
committed
lgj-abi: plan_eval runs tiled and keeps the mask straight into the destination resource
The destination mask is the demanded result, so the lowering ends in Keep and the executor writes it tile by tile into the resource's own words (Out::Mask); the count is a popcount over that sink. The scratch is SLOTS × tile_words_for(rows) — no population-sized scratch, no copy from a slot into the resource. The scalar oracle arm answers the same way through reference_execute_into. Validation stays total before the first tile, so an error leaves dst_mask untouched as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXUahz73MZxtxWcfpHp9dG
1 parent 183745e commit a64fb70

3 files changed

Lines changed: 59 additions & 31 deletions

File tree

‎native/lgj-abi/src/exports.rs‎

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ use std::cell::RefCell;
2626
use std::panic::{catch_unwind, AssertUnwindSafe};
2727

2828
use lance_graph_mask_risc::{
29-
execute, reference_execute, reference_scratch, scratch_words_for, ExecError, LaneRef, Planes,
30-
Scratch, Value,
29+
execute_into, reference_execute_into, scratch_words_for, tile_words_for, ExecError, Foreign,
30+
LaneRef, Out, Planes, Scratch, Value,
3131
};
3232

3333
use crate::abi::*;
@@ -1869,33 +1869,53 @@ fn plan_eval_impl(
18691869

18701870
match path {
18711871
Path::Simd => {
1872-
let need = match scratch_words_for(n_words, plan_lower::SLOTS as usize) {
1872+
// TILED: the scratch is `SLOTS × tile_words_for(rows)` words
1873+
// however many rows the resource holds, and the kept mask is
1874+
// written tile by tile into the destination resource's own words
1875+
// — the demanded sink, and the only population-sized write.
1876+
// Validation is total before the first tile, so an error leaves
1877+
// `dst_mask` byte-for-byte as it was, exactly as before.
1878+
let tile = tile_words_for(rows);
1879+
let need = match scratch_words_for(tile, plan_lower::SLOTS as usize) {
18731880
Some(n) => n,
18741881
None => return LGJ_ERR_LENGTH_OVERFLOW,
18751882
};
1883+
let mut g = match mask.write_mask() {
1884+
Some(g) => g,
1885+
None => return LGJ_ERR_WRONG_RESOURCE_KIND,
1886+
};
1887+
if g.words.len() != n_words {
1888+
return LGJ_ERR_MASK_LENGTH_MISMATCH;
1889+
}
18761890
PLAN_SCRATCH.with(|cell| {
18771891
let mut buf = cell.borrow_mut();
18781892
if buf.len() < need {
18791893
buf.resize(need, 0);
18801894
}
18811895
let mut scratch =
1882-
match Scratch::over(&mut buf[..need], n_words, plan_lower::SLOTS as usize) {
1896+
match Scratch::over(&mut buf[..need], tile, plan_lower::SLOTS as usize) {
18831897
Ok(s) => s,
18841898
Err(e) => return exec_error_to_status(e),
18851899
};
1886-
let count = match execute(&program, &planes, &mut scratch, None) {
1887-
Ok(Value::Count(c)) => c as u64,
1900+
match execute_into(
1901+
&program,
1902+
&planes,
1903+
&Foreign::NONE,
1904+
&mut scratch,
1905+
Out::Mask(&mut g.words),
1906+
) {
1907+
Ok(Value::Mask(_)) => {}
18881908
// The lowering emits exactly one terminal and it is
1889-
// `Count`; any other value means this file built a
1909+
// `Keep`; any other value means this file built a
18901910
// program it did not intend to.
18911911
Ok(_) => return LGJ_ERR_ALLOCATION_FAILED,
18921912
Err(e) => return exec_error_to_status(e),
1893-
};
1894-
let words = match scratch.slot(plan_lower::ACC_SLOT) {
1895-
Some(w) => w,
1896-
None => return LGJ_ERR_ALLOCATION_FAILED,
1897-
};
1898-
publish(&mask, words, count, out_count)
1913+
}
1914+
let count = kernels::simd_popcount(&g.words);
1915+
drop(g);
1916+
// SAFETY: non-null (checked by the caller); written only on success.
1917+
unsafe { *out_count = count };
1918+
LGJ_OK
18991919
})
19001920
}
19011921
// Fork A: the scalar symbol runs mask-risc's row-at-a-time oracle, so
@@ -1905,20 +1925,14 @@ fn plan_eval_impl(
19051925
// bit-packing bug — which is why the allocation gate names
19061926
// `lgj_plan_eval` and only it.
19071927
Path::Scalar => {
1908-
let count = match reference_execute(&program, &planes, None) {
1909-
Ok(Value::Count(c)) => c as u64,
1928+
let mut kept = vec![0u64; n_words];
1929+
match reference_execute_into(&program, &planes, &Foreign::NONE, Out::Mask(&mut kept)) {
1930+
Ok(Value::Mask(_)) => {}
19101931
Ok(_) => return LGJ_ERR_ALLOCATION_FAILED,
19111932
Err(e) => return exec_error_to_status(e),
1912-
};
1913-
let slots = match reference_scratch(&program, &planes) {
1914-
Ok(s) => s,
1915-
Err(e) => return exec_error_to_status(e),
1916-
};
1917-
let words = match slots.get(plan_lower::ACC_SLOT as usize) {
1918-
Some(w) => w.as_slice(),
1919-
None => return LGJ_ERR_ALLOCATION_FAILED,
1920-
};
1921-
publish(&mask, words, count, out_count)
1933+
}
1934+
let count = kernels::simd_popcount(&kept);
1935+
publish(&mask, &kept, count, out_count)
19221936
}
19231937
}
19241938
}

‎native/lgj-abi/src/exports/tests/lowering_convergence.rs‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,22 @@ fn cmp_of(o: &LgjOpDesc) -> Cmp {
156156
/// function on both sides is the whole point of comparing LOWERINGS rather
157157
/// than comparing EXECUTIONS.
158158
fn run(p: &Program, planes: &Planes) -> usize {
159-
let words = planes.n_rows.div_ceil(64);
160-
let slots = p.scratch_slots as usize;
161-
let mut buf = vec![0u64; scratch_words_for(words, slots).expect("sized")];
162-
let mut scratch = Scratch::over(&mut buf, words, slots).expect("carves");
163-
match execute(p, planes, &mut scratch, None).expect("runs") {
159+
// The tiled default scratch. This crate's own lowering ends in `Keep`
160+
// (the destination mask is the demanded sink; the count is its
161+
// popcount), the quack lowering in `Count` — both are read here.
162+
let mut scratch = Scratch::for_program(p, planes.n_rows).expect("addressable");
163+
let mut kept = vec![0u64; planes.n_rows.div_ceil(64)];
164+
match execute_into(
165+
p,
166+
planes,
167+
&Foreign::NONE,
168+
&mut scratch,
169+
Out::Mask(&mut kept),
170+
)
171+
.expect("runs")
172+
{
164173
Value::Count(c) => c,
174+
Value::Mask(_) => ndarray::simd::popcount_batch_u64(&kept) as usize,
165175
other => panic!("not a count: {other:?}"),
166176
}
167177
}

‎native/lgj-abi/src/plan_lower.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,13 @@ pub(crate) fn lower_plan(ops: &[LgjOpDesc]) -> Option<Lowered> {
9696
});
9797
}
9898

99+
// `Keep`, not `Count`: the destination mask IS the demanded result, so
100+
// the executor writes it tile by tile straight into the resource's own
101+
// words (`Out::Mask`) and the count is a popcount over that sink. Under
102+
// tiled execution no population-sized scratch exists to count from.
99103
Some(Lowered::Program(Program::new(
100104
program_ops,
101-
Terminal::Count {
105+
Terminal::Keep {
102106
mask: Operand::Scratch(ACC_SLOT),
103107
},
104108
)))

0 commit comments

Comments
 (0)