Conversation
…r DAG, TensorGraph) Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/7ba255bc-3353-4c28-bf32-317352a74456 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
… operator extensions Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/5861501a-1613-44c9-9fba-bbabda716b7b Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/5861501a-1613-44c9-9fba-bbabda716b7b Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
|
@copilot continue Phase 3 |
…on + reduce/cast/where Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/6521b0eb-cf09-420a-9633-017a83046966 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Phase 3 implemented in commit What's in it:
35 new tests, all green on JVM, JS, and Native. |
|
@copilot is the broadcasting implicit or explicit? I would prefer to avoid numpy style rules and have the user explicitly broadcast when necessary… and be aware of that operation |
…adcastTo(shape) Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/f3af82a4-358e-4942-ab9c-87ed7bd86adf Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Broadcasting is now fully explicit (commit The A new Example usage pattern: val x = b.Expr.param[Double]("x", Shape(Dim.Known(3), Dim.Known(4)))
val s = b.Expr.const[Double](1.0)
val sBig = s.broadcastTo(Shape(Dim.Known(3), Dim.Known(4))) // explicit!
val out = x + sBig |
|
@copilot continue phase 4 |
Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/91859bb2-2cfb-4be3-b534-5a8bceaa5ff4 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Phase 4 implemented in commit New in
35 new tests (AST-shape golden + TypeCheck round-trips), all green on JVM, JS, and Native. |
|
@copilot now phase 5 |
…tors, dead-node pruning) Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/bfa68280-cf1c-4796-8df0-7ad3a993d451 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/bfa68280-cf1c-4796-8df0-7ad3a993d451 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Phase 5 implemented in commit New in
42 new tests (positive/negative golden per rule + idempotence property), all green on JVM, JS, and Native. |
|
@copilot now Phase 6 |
Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/0daebaeb-12c1-4ef3-9ffb-f1c099a7b8c9 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
…mpty groups, add test size check Agent-Logs-Url: https://github.com/Quafadas/vecxt/sessions/0daebaeb-12c1-4ef3-9ffb-f1c099a7b8c9 Co-authored-by: Quafadas <24899792+Quafadas@users.noreply.github.com>
Phase 6 implemented in commit New in
35 new tests, all green on JVM, JS, and Native (238 total). Example behaviour: // sin(x) + cos(x) → one group {Sin, Cos, Add}, inputs=[x]
// sum(sin(x) + 1) → one group {Sin, Add, Reduce(Sum)}, Reduce is the output
// y=sin(x); exp(y)+y → two groups: {Sin} then {Exp, Add}, y materialised once |
Review notes — Phases 2–6 (+ some Phase 7–8)Did a full read-through against Overall vs. the planPhases 2–6 ( Performance — the current executor won't show the win yet (and may regress)The plan correctly defers the actual speed to Phase 8's "second cut" (SIMD), so this is expected, but flagging it so nobody benchmarks it prematurely and is surprised:
The good news: the Possible correctness bug: broadcast in the fused executorI think non-scalar broadcasts produce wrong results in
The interpreter oracle does it properly via right-aligned strided coordinates ( Suggested test (I'd expect it to fail today): val v = b.Expr.param[Double]("v", Shape(Array(Dim.Known(1), Dim.Known(4))))
val g = b.build(v.broadcastTo(Shape(Array(Dim.Known(3), Dim.Known(4)))))
// assert FusedRunner.eval agrees with Interpreter.evalFix options: make Housekeeping
What's great
Suggested before merge
|
Continues the kernel-fusion IR work from #93 (Phase 1) by implementing Phase 2: Graph builder + typed
Expr[A]façade, Phase 3: Shape + dtype inference / checking, Phase 4:MathExpr→TensorExprlowering, Phase 5: Normalisation passes, and Phase 6: Fusion Planner as described invecxt_fusion/Plan.md.Changes
New:
vecxt_fusion/src/builder.scalaDTypeOf[A]— type class mapping Scala types toDTypevalues:Double→F64,Float→F32,Int→I32,Long→I64,Boolean→BoolIsNumeric[A]— sealed evidence trait for numeric operations; instances forDouble,Float,Int(deliberately excludesBoolean)GraphBuilder— mutable builder withHashMap-backed structural hash-consing: identical nodes share oneNodeIdExpr[A]— opaque type (= NodeId) defined insideGraphBuilder; path-dependent, so cross-builder mixing is a compile error+,-,*,/,unary_-(requireIsNumeric[A]evidence); operands must have identical shapes — no implicit broadcasting<,<=,>,>=,===,=!=→Expr[Boolean]; operands must have identical shapes&&,||,unary_!; operands must have identical shapes.sin,.cos,.tan,.exp,.log,.sqrt,.abs,.reciprocal.broadcastTo(targetShape: Shape)— the only way to insert aBCastnode; validates NumPy-style compatibility viaShape.broadcastand throwsIllegalArgumentExceptionif incompatible. Broadcasting is never implicit..castTo[B]— shape-preserving dtype cast.reduceSum,.reduceProduct,.reduceMin,.reduceMax(preserve dtype);.reduceAll,.reduceAny(→Expr[Boolean]);.argMax,.argMin(→Expr[Long])where[A](cond, thenE, elseE)builder method — element-wise conditional select; all three operands must have identical shapesExpr.param[A](name, shape?),Expr.const[A](value),Expr.lift[A](nd: NDArray[A])buildfinaliser → immutableTensorGraphNew:
vecxt_fusion/src/types.scalaadditionsShapeError(message)— error from broadcast shape incompatibilityShape.broadcast(a, b): Either[ShapeError, Shape]— NumPy-style rules overDimvariants:Knowndims with 1-broadcasting,Symdims unified by name,UnknownpropagatesNew:
vecxt_fusion/src/typecheck.scalaTypeErrorsealed ADT withat: NodeId+message: String; three cases:ShapeMismatch,DTypeMismatch,InvalidAxesTypeCheck.infer(graph): Either[TypeError, TensorGraph]— walks every node in topological order, re-derives each node's result type from its children, and verifies against the storedtpefield. Rules:Binary: operands must have equal shapes (use.broadcastToexplicitly before combining); dtype rules per opUnary: shape-preserving;NotrequiresBoolinputCast: shape-preserving, dtype = targetBCast: source shape must broadcast to target shapeReduce: axes within rank;Sum/Product/Min/Max→ input dtype;All/Any→ Bool;ArgMax/ArgMin→ I64Where: condition must be Bool; all operands must have identical shape and dtypeNew:
vecxt_fusion/src/lower.scalaTypeEnv = Map[String, TType]— maps symbol names to their tensor type (dtype + shape)LowerErrorsealed ADT:UnknownSymbol,UnsupportedBinder,NonSemanticSuperscript,UnknownFunction,UnsupportedNodeLower.lower(expr: MathExpr[Double], env: TypeEnv): Either[LowerError, TensorGraph]— structural, fail-fast lowering with hash-consing:Number(v)→Const(v, f64[])Symbol(n)/Constant(n)→Param(n, env(n))(env lookup;UnknownSymbolif missing)Add/Sub/Mul/Div/Pow→Binary;Neg→Unary(Neg)Fraction(a, b)→Binary(Div, a, b)Group/BracketGroup/Color/Style/Enclose→ transparent (recurse into content)Root(None, r)→Unary(Sqrt, r);Root(Some(_), _)→UnsupportedNodeSuperscript(b, Number(e))→Binary(Pow, b, Const(e)); non-numeric exponent →NonSemanticSuperscriptFunctionCallfor sin/cos/tan/exp/log/ln/sqrt/abs →Unary; unknown name or wrong arity →UnknownFunctionExprSeq([e])→ transparent; other lengths →UnsupportedNodeSum/Integral/Subscript/Over/Under/SubSup→UnsupportedBinderOperator/TextNode→UnsupportedNodeNew:
vecxt_fusion/src/normalize.scalaNormalize.run(graph): TensorGraph— single-pass DAG walk applying all normalisation rules, returning a fresh compacted graph with no dead nodes:Unary/Binary(all numeric ops + comparisons) andBoolUnary/Binary(Not,And,Or,Eq,Neq) collapsed to a singleConstnodex+0→x,0+x→x,x-0→x,x*1→x,1*x→x,x/1→x,pow(x,1)→xx*0→0,0*x→0,0/Const(k≠0)→0,pow(x,0)→1,pow(1,x)→1;0/0is constant-folded to IEEE NaN (correct behaviour);0/Paramis left as-is (x could be zero at runtime)Whereshort-circuit —where(true,x,y)→x;where(false,x,y)→yCastelision —cast(T, x: T)→xwhen source and target dtype are identicalAdd,Mul,Min,Max,Eq,Neq,And,Or: operands stored in ascendingNodeIdorder sox+yandy+xhash-cons to the same noderun(run(g)) == run(g)verified as a property testNew:
vecxt_fusion/src/fuse.scalaGroupId(i: Int)— value class;iis the index into the execution-orderedgroupsvectorFusionGroup(nodeIds, inputs, output)— a set of nodes that execute as one fused kernel;inputs= boundary inputs from other groups or leaves;output=nodeIds.last(last computed, highest topo index)FusionPlan(graph, groups, assignment)— the result of fusion planning;assignment: Map[NodeId, GroupId]covers all non-leaf nodes;groupOf(id)returnsNonefor leavesCostModelstub —estimatedFlops(transcendentalsExp/Log/Sin/Cos/Tan= 20× per element, all others = 1×) andestimatedBytesRead(per-input shape × 8 bytes for F64)FusionPlanner.plan(graph): FusionPlan— top-down greedy algorithm:Unary/Binary/Where/Cast/BCast) + one terminalReduceper groupReduceproducers —Reduceis always the terminal/output node of its group when fusedNodeIdexecution orderUpdated:
vecxt_fusion/package.millAdded
vecxt.jsandvecxt.nativemodule dependencies (previously onlyvecxt.jvmwas wired).New tests
vecxt_fusion/test/src/builder.test.scala— 35 Phase 2 tests: hash-consing, all operators, shape/dtype preservation, type-level compile-error assertionsvecxt_fusion/test/src/typecheck.test.scala— 35 Phase 3 tests:Shape.broadcastgolden cases (scalar, rank-extension, symbolic unification).broadcastTogolden:scalar.broadcastTo([3,4])thenx + sBig— user is in full control of BCast insertionIllegalArgumentException(no silent broadcasting)TypeCheck.infergolden:Reduce(Sum, x: f64[3,4], axes=[0])→f64[4]Castround-trip:F64 → Bool → F64allowedvecxt_fusion/test/src/lower.test.scala— 35 Phase 4 tests:Number,Symbol, unknown symbol errorNeg,Fraction,PowGroup,BracketGroup,Color,Style,Enclose,Root(None, _)Superscriptwith numeric exponent →Binary(Pow); non-numeric →NonSemanticSuperscripterrorFunctionCallnames (sin/cos/tan/exp/log/ln/sqrt/abs); unknown name/arity → errorUnsupportedBindererrorx + xshares oneParamnode;(x+y)+(x+y)shares oneBinarynodeTypeCheck.infervecxt_fusion/test/src/normalize.test.scala— 42 Phase 5 tests:run(run(g)) == run(g)for all rule variantsTypeCheck.inferstill passes on normalised graphsvecxt_fusion/test/src/fuse.test.scala— 35 Phase 6 tests:nodeIdsequalsassignmentkey setNodeIdorder; producer group precedes consumer groupoutput == nodeIds.last,nodeIdsascending, no input/node overlapgroupOfAPI:Nonefor leaves,Somefor non-leaves, barrier placement in own groupCast,BCast,Where) fuse correctly with their producersReduceproducers; Reduce-seeded groups absorb elementwise chainsCostModelassertions:nodeFlops(per op type),estimatedFlops,estimatedBytesReadNormalize: planning after normalisation works correctly including dead-node removalTest Results
All 200 tests (18 Phase 1 + 35 Phase 2 + 35 Phase 3 + 35 Phase 4 + 42 Phase 5 + 35 Phase 6) pass on JVM, JS, and Native.