spcl / spcl/spada

SpaDA emits raw `[1]f32` refs where CSL requires DSD/DSR operands for async fabric operations

Open
#76 0 comments 0 reactions 1 assignee View on GitHub

@tbennun is already working on this.

Since Sep 15, 2026.

Dominant language
Python
Stars
10
Forks
2
Avg merge
1h 43m
Merged PRs (30d)
1

Description

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?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.