TL;DR
sptlc lowers any per-PE single-element buffer (e.g. a 1-wide halo cell f32[1]) into a bare [1]f32 identifier passed straight into @fmovs on a fabout_dsd / fabin_dsd. cslc rejects that: async fabric ops require DSD/DSR operands. Wrapping the buffer in a mem1d_dsd at emission time fixes it and produces bitwise-correct output. Repro below.
Environment
| Component |
Value |
| SpaDA (this checkout) |
88e79fb3d123d7278f70625fdfdc39fc5a34f4d2 , 2026-04-20, main HEAD |
| Cerebras SDK |
Cerebras-SDK-1.4.0 |
| SDK container image |
sdk-cbcore-202505010205-2-ef181f81.sif (SHA256 9df94b2b…) |
cslc binary |
SHA256 e49dca08… |
cs_python binary |
SHA256 62ae6c68… |
| Python (SpaDA runtime) |
≥ 3.9 required (SpaDA source uses PEP 585 generic subscripting; 3.8 fails at import) |
| Python (in SDK container) |
3.8.16 (only affects cs_python) |
| CSL target arch |
--arch=wse2 (SpaDA default per spada/syntax/csl/constants.py:ARCH). Bug also reproduces on --arch=wse3 with the identical cslc message. |
| OS (host tested) |
Rocky Linux 8.10, kernel 4.18.0-553.53.1.el8_10.x86_64 |
How the CSL was generated
The reproducer bundle ships pre-generated CSL, but the CSL is produced by SpaDA's sptlc CLI (spada/cli/compiler.py, entrypoint sptlc from setup.py):
# Build spada and compile to CSL
pip install -e .
# Lower one .spada file to a folder of .csl + metadata.json (does not invoke cslc)
sptlc kernel.spada out/ --generate-only
sptlc internally runs the CSL lowering pipeline (spada/lowering/spatial_ir_to_csl.py:lower_spatial_ir_to_csl) with these default settings — none disabled in this repro:
disable_benchmarking = False
disable_asynchronous = False
disable_dsd = False
disable_map = False
disable_task_fusion = False
disable_copy_elision = False
disable_task_recycling = False
Without --generate-only it additionally invokes:
For the reproducer below, the equivalent explicit cslc invocation is:
cslc --arch=wse2 ./layout.csl \
--fabric-dims=5,2 --fabric-offsets=4,1 \
-o out --memcpy --channels=1
Input — minimal .spada
halo_p2.spada (1D 3-point stencil, per-PE tile width 2, halo width 1, over 2 PEs — the smallest kernel that declares a single-element f32[1] buffer and uses it in a send/receive):
kernel @stencil<>(stream<f32, 2>[2, 1] readonly in0_in,
stream<f32, 2>[2, 1] writeonly out0_out) {
place i16 x, i16 y in [0:2, 0] {
f32[2] in0
f32[1] in0_halo_lo
f32[1] in0_halo_hi
f32[2] out0
f32[1] in0_edge_hi
f32[1] in0_edge_lo
}
phase {
compute i16 x, i16 y in [0:2, 0] {
await receive(in0, in0_in[x, y])
}
}
phase {
dataflow i16 x, i16 y in [0:2, 0] {
stream<f32> in0_east = relative_stream(1, 0) { hops = [(1, 0)], channel = 0 }
stream<f32> in0_west = relative_stream(-1, 0) { hops = [(-1, 0)], channel = 1 }
}
compute i16 x, i16 y in [0, 0] {
in0_edge_hi[0] = in0[1]
in0_halo_lo[0] = 0.0
completion in0_send_east = send(in0_edge_hi, in0_east)
completion in0_recv_hi = receive(in0_halo_hi, in0_west)
await in0_send_east
await in0_recv_hi
}
compute i16 x, i16 y in [1, 0] {
in0_edge_lo[0] = in0[0]
in0_halo_hi[0] = 0.0
completion in0_send_west = send(in0_edge_lo, in0_west)
completion in0_recv_lo = receive(in0_halo_lo, in0_east)
await in0_send_west
await in0_recv_lo
}
}
phase {
compute i16 x, i16 y in [0:2, 0] {
out0[0] = ((in0_halo_lo[0] + in0[0]) + in0[1])
out0[1] = ((in0[0] + in0[1]) + in0_halo_hi[0])
await send(out0, out0_out[x, y])
}
}
}
Output SpaDA emits
sptlc halo_p2.spada out/ --generate-only produces :
var in0_halo_lo: [1]f32;
var in0_halo_hi: [1]f32;
var in0_edge_hi: [1]f32; // single-element buffers — no MemoryDSD registered
var in0_in: [2]f32;
var out0_out: [2]f32;
// Only the fabric DSDs are emitted. NO mem1d_dsd for in0_edge_hi / in0_halo_hi.
const in0_east__1_out_dsd = @get_dsd(fabout_dsd, .{ .extent = 1,
.fabric_color = in0_east__1_color_out, .output_queue = @get_output_queue(2) });
const in0_west__1_in_dsd = @get_dsd(fabin_dsd, .{ .extent = 1,
.fabric_color = in0_west__1_color_in, .input_queue = @get_input_queue(0) });
task task_slot_0() void {
in0_edge_hi[0] = in0_in[1];
in0_halo_lo[0] = 0.0;
@fmovs(in0_east__1_out_dsd, in0_edge_hi, .{ .async = true, .activate = task_1_id });
@fmovs(in0_halo_hi, in0_west__1_in_dsd, .{ .async = true, .unblock = task_1_id });
@activate(task_2_id);
}
The two async @fmovs at lines 34–35 pass in0_edge_hi and in0_halo_hi — raw [1]f32 identifiers — as the memory operand.
What cslc says
$ cslc --arch=wse2 ./layout.csl --fabric-dims=5,2 --fabric-offsets=4,1 -o out --memcpy --channels=1
./code_0_0.csl:34:5: error: only DSD/DSR operands are allowed for async operations
@fmovs(in0_east__1_out_dsd, in0_edge_hi, .{ .async = true, .activate = task_1_id });
^
./layout.csl:12:13: error: semantic error in module imported here
@set_tile_code(pe_x, pe_y, "code_0_0.csl", .{ .memcpy_params = memcpy.get_params(pe_x), });
^
The same defect surfaces with a different cslc message for a synchronous @fmovs — where pad_zero: [1]f32 is passed to @fmovs(pad_zero, 0.0);:
./code_1_0.csl:46:5: error: operand types do not match expectations
@fmovs(pad_zero, 0.0);
^
./code_1_0.csl:46:5: note: got type(s): [1]f32, comptime_float
./code_1_0.csl:46:5: note: expected type(s): DSD/DSR, DSD/DSR
./code_1_0.csl:46:5: note: expected type(s): DSD/DSR, f32
./code_1_0.csl:46:5: note: expected type(s): *f32, DSD/DSR
Same root cause: SpaDA emits the bare identifier where cslc requires a DSD.
What CSL actually needs (Ideal version made to pass the CSLC compiler stage successfully)
Adding a mem1d_dsd wrapper per single-element buffer and rewriting the @fmovs operands to use the wrapper name compiles cleanly:
const in0_edge_hi_dsd = @get_dsd(mem1d_dsd, .{ .tensor_access = |__index|{1} -> in0_edge_hi[__index] });
const in0_halo_hi_dsd = @get_dsd(mem1d_dsd, .{ .tensor_access = |__index|{1} -> in0_halo_hi[__index] });
@fmovs(in0_east__1_out_dsd, in0_edge_hi_dsd, .{ .async = true, .activate = task_1_id });
@fmovs(in0_halo_hi_dsd, in0_west__1_in_dsd, .{ .async = true, .unblock = task_1_id });
Result on the same cslc command line above:
[INFO] === Beginning compilation ===
[INFO] Compilation successful
And the compiled ELF passes the host-reference check bitwise:
$ cs_python check.py --kind stencil --elf-dir out --meta metadata.json
kernel=stencil kind=stencil grid=2x1 per_pe=2 N=4
launch...
d2h...
got [0:6] = [ 1. 4. 9. 13. 15. 9.]
expect[0:6] = [ 1. 4. 9. 13. 15. 9.]
BITWISE PASS: 4/4
Is this a SPADA bug or an unsupported feature?
TL;DR
sptlclowers any per-PE single-element buffer (e.g. a 1-wide halo cellf32[1]) into a bare[1]f32identifier passed straight into@fmovson afabout_dsd/fabin_dsd.cslcrejects that: async fabric ops require DSD/DSR operands. Wrapping the buffer in amem1d_dsdat emission time fixes it and produces bitwise-correct output. Repro below.Environment
88e79fb3d123d7278f70625fdfdc39fc5a34f4d2, 2026-04-20,mainHEADCerebras-SDK-1.4.0sdk-cbcore-202505010205-2-ef181f81.sif(SHA2569df94b2b…)cslcbinarye49dca08…cs_pythonbinary62ae6c68…cs_python)--arch=wse2(SpaDA default perspada/syntax/csl/constants.py:ARCH). Bug also reproduces on--arch=wse3with the identicalcslcmessage.How the CSL was generated
The reproducer bundle ships pre-generated CSL, but the CSL is produced by SpaDA's
sptlcCLI (spada/cli/compiler.py, entrypointsptlcfromsetup.py):sptlcinternally runs the CSL lowering pipeline (spada/lowering/spatial_ir_to_csl.py:lower_spatial_ir_to_csl) with these default settings — none disabled in this repro:disable_benchmarking = Falsedisable_asynchronous = Falsedisable_dsd = Falsedisable_map = Falsedisable_task_fusion = Falsedisable_copy_elision = Falsedisable_task_recycling = FalseWithout
--generate-onlyit additionally invokes:For the reproducer below, the equivalent explicit
cslcinvocation is:cslc --arch=wse2 ./layout.csl \ --fabric-dims=5,2 --fabric-offsets=4,1 \ -o out --memcpy --channels=1Input — minimal
.spadahalo_p2.spada(1D 3-point stencil, per-PE tile width 2, halo width 1, over 2 PEs — the smallest kernel that declares a single-elementf32[1]buffer and uses it in asend/receive):Output SpaDA emits
sptlc halo_p2.spada out/ --generate-onlyproduces :The two async
@fmovsat lines 34–35 passin0_edge_hiandin0_halo_hi— raw[1]f32identifiers — as the memory operand.What
cslcsaysThe same defect surfaces with a different
cslcmessage for a synchronous@fmovs— wherepad_zero: [1]f32is passed to@fmovs(pad_zero, 0.0);:Same root cause: SpaDA emits the bare identifier where
cslcrequires a DSD.What CSL actually needs (Ideal version made to pass the CSLC compiler stage successfully)
Adding a
mem1d_dsdwrapper per single-element buffer and rewriting the@fmovsoperands to use the wrapper name compiles cleanly:Result on the same
cslccommand line above:And the compiled ELF passes the host-reference check bitwise:
Is this a SPADA bug or an unsupported feature?