Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,9 +892,16 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
`min(a, b)` compares with `<` and keeps the first element in iteration order on ties (including
`+0.0` versus `-0.0`), NaN behaviour depends on position because comparisons with NaN are false,
and chunked combination must reproduce that chain bit for bit; if an implementation cannot, they
are left to a later step. Step 2 implements `+` and `*` only (min/max follow in step 3 below): the compiled subset has no bitwise
operator (the front end skips a function using `&`, `|` or `^` on ints, there is no IR operation),
so `& | ^` are left until the subset has them.
are left to a later step. Step 2 implements `+` and `*` (min/max follow in step 3, `& | ^` in step 4 below).
**Bitwise (step 4).** `acc = acc & e`, `acc | e`, `acc ^ e` (either operand order; the augmented
`acc &= e` lowers to the same shape) on an i64 accumulator, with `e` the step 2 i64 expression set
extended with the N-16 operators `& | ^` and `~`. Shifts are not accepted in `e`: their overflow
and negative-count deopt is not a worker failure, so such a loop stays sequential. A chunk folds
from the identity (-1 for `&`, 0 for `|` and `^`) and the partials combine in chunk order; these
operators are exactly associative and commutative and never overflow, so the combination cannot
fail. Only an overflow inside `e` (for example `xs[i] * c`) discards the region and reruns the loop
sequentially, as for `+`. They may share a loop with `+ *` and min/max accumulators. The accumulator
rules are step 2's (read nowhere else in the body, one operator per accumulator).
**`min`/`max` (step 3).** The compiled subset has no `min(a, b)` / `max(a, b)` call (it stays a
generic object call, sequential). What it does compile is Python's own definition of
`min(acc, e)`, the conditional update `if e < acc: acc = e` (`e > acc` for max; `acc > e` and
Expand Down Expand Up @@ -941,7 +948,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
chunk, NaN/inf/-0.0), identical bytes for every thread count, failure at first/middle/last
iteration, negative qualification cases, disabled-path test, reductions, race and GC tests,
TSan/ASan, N-14, signals, fork and shutdown, and measurement tests for the threshold.
`Status: steps 1, 2 and 3a implemented (#277): element-wise F64 stores a[i] = <F64 + - * and negation of constants, locals and b[i]> into list copy-in or local arrays (no failure path: eligible loops cannot fail or deopt); i64 reductions acc += e and acc *= e with the overflow rerun above (stores and reductions never in one loop); i64 and f64 min/max as `if e < acc: acc = e` (f64 with a NaN reruns sequentially); fork, signal and shutdown tests; & | ^ reductions, float + and * reductions, the poll inside a region and the rest planned (design)`.
`Status: steps 1, 2, 3a and 4 implemented (#277): element-wise F64 stores a[i] = <F64 + - * and negation of constants, locals and b[i]> into list copy-in or local arrays (no failure path: eligible loops cannot fail or deopt); i64 reductions acc += e and acc *= e with the overflow rerun above (stores and reductions never in one loop); i64 and f64 min/max as `if e < acc: acc = e` (f64 with a NaN reruns sequentially); i64 bitwise reductions acc &= e, |= e, ^= e (identity -1 for &, 0 for | and ^; no shifts in e; test_parallel_loops.py bitwise tests); fork, signal and shutdown tests; float + and * reductions, the poll inside a region and the rest planned (design)`.

- **N-16** TypedPython bitwise operators on ints (#289; INTENT §1.4: compiled code computes what CPython
computes). `&`, `|`, `^`, unary `~`, `<<`, `>>` and the augmented forms `&= |= ^= <<= >>=` are in the compiled
Expand Down
4 changes: 2 additions & 2 deletions python-multiplatform-ksp/src/main/python/typedpython/cgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -2188,7 +2188,7 @@ def key(x):
continue
v = st.value if isinstance(st, ir.Assign) else None
if (not isinstance(v, ir.BinOp) or st.target != acc or v.op.name != opn
or opn not in ("ADD", "MUL") or v.type is not Type.I64
or opn not in ("ADD", "MUL", "AND", "OR", "XOR") or v.type is not Type.I64
or self.types.get(acc) is not Type.I64 or acc == s.var):
raise CGenError(f"{f.name}: parallel reduction statement is not `acc = acc op e`")
if isinstance(v.left, ir.Local) and v.left.name == acc:
Expand Down Expand Up @@ -2282,7 +2282,7 @@ def key(x):
" uint64_t tp_k;", *decls]
if reds:
out += [" int tp_rc = 0;", " int tp_s = 0;"]
out += [f" {_CT[self.types[acc]]} {_ident('l', acc)} = {'1' if opn == 'MUL' else '0'};"
out += [f" {_CT[self.types[acc]]} {_ident('l', acc)} = { {'MUL': '1', 'AND': '-1'}.get(opn, '0') };"
for acc, opn, _ in reds]
out += [f" int tp_has{j} = 0;" for j, (_, opn, _) in enumerate(reds) if opn in ("MIN", "MAX")]
out.append(" (void)tp_rc; (void)tp_s;")
Expand Down
2 changes: 1 addition & 1 deletion python-multiplatform-ksp/src/main/python/typedpython/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ class ParInfo:
read: tuple[str, ...]
weight: int = 1
# Step 2 (#277): the i64 reductions `acc = acc op e` of a loop whose body is only such statements,
# as sorted (acc, BinOpKind name) pairs, op ADD or MUL. Then `stored` is empty. Each chunk computes
# as sorted (acc, BinOpKind name) pairs, op ADD, MUL, AND, OR or XOR (min/max as MIN, MAX). Then `stored` is empty. Each chunk computes
# its partial from the op's identity with the sequential code's overflow checks; an overflow in any
# chunk or in the combination discards the region and the sequential loop runs from its start.
reduce: tuple[tuple[str, str], ...] = ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ for the epoch's increment. `tp_runtime.h` needs the GCC/Clang `__atomic` builtin

## Parallel regions (SPEC N-15, issues #151, #277)
A `ForRange` the verifier marked `par` (`ir.ParInfo`; marked function, `a[i] = F64 expression` stores
or i64 `+ *` reductions, or min/max `if e < acc: acc = e` on an i64 or f64) runs its iterations on per-region C threads above a work threshold. Workers touch no Python
or i64 `+ * & | ^` reductions, or min/max `if e < acc: acc = e` on an i64 or f64) runs its iterations on per-region C threads above a work threshold. Workers touch no Python
object and no Python API, are not Python threads and never outlive the call.
#define TP_PAR_MIN_WORK 65536 /* PLACEHOLDER, to be measured on #151: weighted iterations (trip count * ParInfo.weight) */
#define TP_PAR_MAX_THREADS 8 /* default cap; TYPEDPYTHON_THREADS overrides (1 disables), hard cap 64 */
Expand All @@ -512,7 +512,7 @@ object and no Python API, are not Python threads and never outlive the call.
void tp_par_stats(int64_t out[4]); /* regions with workers, workers started, failed creations, reduction reruns */
#define TP_PAR_NONE 0 / TP_PAR_OK 1 / TP_PAR_BAD 2 /* per-chunk state of a reduction chunk (zero-initialised array of TP_PAR_HARD_CAP) */
#define TP_PAR_OP_ADD 0 / TP_PAR_OP_MUL 1
int tp_par_combine(int op, int64_t *acc, const int64_t *part, const unsigned char *st); /* acc op= part[0], part[1], ... in chunk order, checked; 1 if a chunk was BAD or it overflowed (acc untouched) */
int tp_par_combine(int op, int64_t *acc, const int64_t *part, const unsigned char *st); /* op: TP_PAR_OP_ADD/MUL/AND/OR/XOR; acc op= part[0], part[1], ... in chunk order, checked (AND/OR/XOR never overflow); 1 if a chunk was BAD or it overflowed (acc untouched) */
void tp_par_rerun_note(void); /* counts a discarded reduction region */
`tp_par_run` always completes the whole range. The calling thread runs the first chunk; a chunk whose
`pthread_create` failed runs on the calling thread after it (fewer workers, down to none: the sequential
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3170,8 +3170,14 @@ static inline int tp_par_run(tp_par_fn fn, const void *ctx, uint64_t n)
#define TP_PAR_BAD 2
#define TP_PAR_OP_ADD 0
#define TP_PAR_OP_MUL 1
#define TP_PAR_OP_AND 2
#define TP_PAR_OP_OR 3
#define TP_PAR_OP_XOR 4

static inline int64_t tp_par_identity(int op) { return op == TP_PAR_OP_MUL ? 1 : 0; }
static inline int64_t tp_par_identity(int op)
{
return op == TP_PAR_OP_MUL ? 1 : op == TP_PAR_OP_AND ? -1 : 0;
}

/* acc = acc op part[0] op part[1] ... in chunk order, checked like the sequential code. Returns 0 and
* stores the result in *acc, or 1 (a chunk failed or the combination overflowed) leaving *acc alone. */
Expand All @@ -3185,6 +3191,10 @@ static inline int tp_par_combine(int op, int64_t *acc, const int64_t *part, cons
}
}
for (k = 0; k < TP_PAR_HARD_CAP && st[k] == TP_PAR_OK; k++) {
if (op >= TP_PAR_OP_AND) { /* bitwise: exact, never overflows */
v = op == TP_PAR_OP_AND ? (v & part[k]) : op == TP_PAR_OP_OR ? (v | part[k]) : (v ^ part[k]);
continue;
}
if (op == TP_PAR_OP_MUL ? tp_mul_i64(v, part[k], &w) : tp_add_i64(v, part[k], &w)) {
return 1;
}
Expand Down
20 changes: 14 additions & 6 deletions python-multiplatform-ksp/src/main/python/typedpython/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,13 @@
distinct lists by the entry guard, local arrays are distinct allocations), and a read of the
stored array itself is the same element the iteration writes.
* Step 2 (reductions): or the body is a non-empty sequence of `acc = acc op e` (`_par_reduce`): op
ADD or MUL, each `acc` a distinct i64 local, definitely assigned before the loop, not an array,
ADD, MUL, AND, OR or XOR, each `acc` a distinct i64 local, definitely assigned before the loop, not an array,
loop variable or entry global, and read nowhere else in the body (not in any `e`); `e` built from
i64 Const, i64 Local (the loop variable, or a scalar nothing in the body assigns), `b[var]`
proven on an i64 non-buffer list or local array, and i64 + - *. Stores and reductions are never
proven on an i64 non-buffer list or local array, and i64 + - * & | ^ and ~ (step 4; no shifts). Stores and reductions are never
mixed in one loop. Those nodes can overflow, which is not a failure of the region: the runtime
discards it and reruns the loop sequentially. (& | ^ are not in the compiled subset: the front
end lowers no bitwise operator, so there is no such reduction to mark.)
discards it and reruns the loop sequentially. Step 4: acc & | ^ e never overflow (identity -1 for &,
0 for | and ^); only an overflow inside `e` reruns.
* Step 3 (min/max): a statement of the reduction body may be `if e < acc: acc = e` / `e > acc` (either
operand order, no else), `acc` an i64 or f64 local, `e` of its type (`_par_minmax`). That is Python's
`min(acc, e)`; the f64 NaN case reruns sequentially at run time (SPEC N-15).
Expand Down Expand Up @@ -310,6 +310,12 @@
PAR_LOOP = "verify/par-loop"


# SPEC N-15 steps 2 and 4: the reduction operators, and the i64 operators allowed in an element expression
# (no shifts: their deopt is not a worker failure).
_PAR_RED_OPS = (BinOpKind.ADD, BinOpKind.MUL, BinOpKind.AND, BinOpKind.OR, BinOpKind.XOR)
_PAR_I64_OPS = (BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL, BinOpKind.AND, BinOpKind.OR, BinOpKind.XOR)


@dataclass(frozen=True)
class Diagnostic:
function: str
Expand Down Expand Up @@ -1754,7 +1760,7 @@ def _par_reduce(self, s: ForRange, body: tuple, state: _State, weight: list, arr
mm[hit[0]] = hit[2]
continue
if (type(st) is not Assign or type(st.value) is not BinOp or st.value.type is not Type.I64
or st.value.op not in (BinOpKind.ADD, BinOpKind.MUL) or st.value.np
or st.value.op not in _PAR_RED_OPS or st.value.np
or st.target in accs):
return None
accs[st.target] = st.value.op.name
Expand All @@ -1781,8 +1787,10 @@ def ok(e: object) -> bool:
return False
if t is BinOp:
return (e.type is Type.I64 and e.left.type is Type.I64 and e.right.type is Type.I64
and e.op in (BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL) and not e.np
and e.op in _PAR_I64_OPS and not e.np
and ok(e.left) and ok(e.right))
if t is UnaryOp: # ~x of an i64: cannot fail (SPEC N-16)
return e.type is Type.I64 and e.op is UnaryOpKind.INVERT and ok(e.operand)
return False

def reads_acc(e: object) -> bool:
Expand Down
Loading