better throughput scheduling with steady state cost model, now limited to spatial
This commit is contained in:
@@ -17,7 +17,7 @@ Boolean switches default to off unless stated otherwise.
|
||||
| `--pim-only-codegen` | Run the codegen tail on bufferized Pim IR. |
|
||||
| `--pim-emit-json` | Also emit per-core JSON instructions. |
|
||||
| `--pim-dump-ir` | Write diagnostic IR snapshots with elided large constants; default: off. |
|
||||
| `--pim-periodic-diagnostics` | Write the optional pre-codegen recurrence report; default: off. |
|
||||
| `--pim-periodic-diagnostics` | Write optional recurrence and Spatial execution-work reports; default: off. |
|
||||
| `--pim-export-spatial-dataflow=none\|spatial1\|spatial2\|spatial3\|spatial4\|all` | Export graph, trivially merged, scheduled, and realized CSV snapshots under `reports/`; default: `none`. |
|
||||
| `--pim-conv-lowering=STRATEGY` | `auto` (default), `legacy`, `depthwise`, `packed-im2col`, `streamed-patch`, `streamed-packed`, `output-channel-tiled`, `input-k-tiled`, or `tiled-2d`. |
|
||||
| `--pim-conv-im2col-max-elements=N` | Global im2col limit before streaming; default: 1048576. |
|
||||
|
||||
@@ -72,6 +72,59 @@ byte-sized elements. Blueprint assembly charges per fragment. A full fragment
|
||||
receive directly into assembly storage omits duplicate endpoint-memory edge
|
||||
cost; later copy elimination can leave this estimate conservative.
|
||||
|
||||
`CopyAccessPlan` shares ordered contiguous-run geometry between projection
|
||||
costing and PIM copy normalization. Its inputs are a logical copy shape and
|
||||
proven source and destination strides, in elements. The immutable result keeps
|
||||
outer dimensions, run count, run length, and relative offsets compact. Building
|
||||
the plan takes linear time and space in rank, independent of loop trip counts.
|
||||
It creates no IR and does not prove aliasing, storage ownership, or copy legality.
|
||||
PIM bufferization still owns those decisions and the mapping to byte accesses.
|
||||
The plan alone does not describe a complete scheduled memory-request stream.
|
||||
Construction rejects an unrepresentable maximum relative element offset,
|
||||
including the last element of each run. Callers still check byte scaling and
|
||||
base-address addition.
|
||||
Projection costing uses checked row-major strides. Unsupported flattened
|
||||
offsets or strides produce at most one warning per graph cost query. The query
|
||||
uses one separately rounded request per element as a conservative service bound.
|
||||
It does not reject otherwise valid Spatial IR. Target lowering retains its own
|
||||
address and layout legality checks. Unsupported estimates and malformed IR are
|
||||
different conditions.
|
||||
|
||||
The PIM adapter's `planComputeInputMaterialization` reads scheduled Spatial
|
||||
regions without changing them. It returns `Unused`, `ForwardConstant`,
|
||||
`ForwardFunctionInput`, or `CopyAtEntry`. Function-input normalization forwards
|
||||
host inputs into compute bodies before entry-copy materialization.
|
||||
Single-core lowering forwards used arithmetic constants. Batch lowering copies
|
||||
remaining inputs at body entry, including constants. Lowering and costing
|
||||
consume the same decisions. The query has constant time and space per
|
||||
input and does not expand lanes or loops. Abstract target work extraction uses
|
||||
the same query. Subsequent canonicalization can remove uses.
|
||||
`getReusableOutputTensor` shares read-only destination selection with vector
|
||||
lowering. It identifies a tensor operand or requires fresh storage. It does not
|
||||
prove a physical alias, assign an address, or replace one-shot bufferization.
|
||||
The result is valid only for the current SSA graph. Changes to uses, operation
|
||||
order, loop yields, or operands require recomputation. Lowering consumes this
|
||||
decision immediately; it does not cache values across rewrites. Current PIM
|
||||
bufferization consumes the explicit destination ties introduced by target
|
||||
lowering. Output-copy forwarding also consumes the resulting allocation,
|
||||
subview, copy, and dominance facts. An early destination choice alone cannot
|
||||
describe those physical aliases or the final storage lifetimes.
|
||||
Logical storage identity and physical address equality are different facts.
|
||||
Local-memory planning can assign the same address to distinct allocations whose
|
||||
lifetimes do not overlap. Scalar register setup therefore needs placement and
|
||||
instruction-order information in addition to logical alias relationships.
|
||||
Abstract cost estimates need not reconstruct these addresses.
|
||||
|
||||
This input plan does not count physical constant loads. A forwarded host
|
||||
constant can need a new local buffer at each use. Bufferization, host folding,
|
||||
alias analysis, and memory normalization determine the resulting requests.
|
||||
`isZeroBitPatternSplat` shares constant-content classification between graph
|
||||
accounting and code generation. PIM replaces eligible positive-zero and integer-zero
|
||||
loads with local fills. Negative floating zero retains its distinct bit pattern.
|
||||
Constant content is candidate-independent; input placement and buffer reuse
|
||||
depend on the realized candidate. Production uses these shared decisions when
|
||||
extracting Spatial execution events for the queue estimate.
|
||||
|
||||
Execution transpose costs share `getTransposeRun` with lowering: round memory
|
||||
reads/writes per run and add vector execution when source stride is not one.
|
||||
The [Pimsim vector unit](../../backend-simulators/pim/pimsim-nn/src/core/component/VectorUnit.cpp)
|
||||
@@ -118,3 +171,89 @@ scheduling overview. Cached PEFT center distances avoid a topology scan per task
|
||||
projection and priority analyses share/cache work rather than revisiting every
|
||||
template body per chunk. Ready-transfer signature searches remain another
|
||||
potential compile-time hotspot.
|
||||
|
||||
### Spatial execution costs
|
||||
|
||||
`SchedulingTarget::estimateRealizedScheduleCost` is a read-only query on realized Spatial IR.
|
||||
The PIM adapter extracts compact ordered execution events without constructing PIM operations.
|
||||
It uses physical core assignments, communication, input-materialization decisions, and shared copy geometry.
|
||||
No physical buffer addresses are required.
|
||||
The shared engine models controller arbitration, source FIFOs, synchronization, and repeated generations.
|
||||
Both arbitration policies are sensitivity checks, not guaranteed bounds.
|
||||
The engine returns typed intervals and recurrence evidence before diagnostic serialization.
|
||||
The target adapter adds static lowering support. Spatial owns the selection policy margin.
|
||||
JSON fields are report output and do not control availability or eligibility.
|
||||
Typed evaluation completes before reporting. Serialization never runs queue analysis.
|
||||
Only optional diagnostics construct fingerprints, traces, and per-core resource reports.
|
||||
|
||||
Normal compilation extracts costs for multiple candidates and lowers only the selected schedule.
|
||||
A single candidate bypasses the cost query entirely.
|
||||
Candidate summaries distinguish available timing, static lowering support, and proved modeled recurrence.
|
||||
Unknown support remains explicit. All candidates must satisfy these checks before comparison.
|
||||
`Unsupported` identifies a known static violation, including invalid or repeated physical-core assignments.
|
||||
`Unknown` identifies missing evidence or an unmodeled form, rather than a proven backend rejection.
|
||||
`Supported` covers checked static forms and does not prove final allocation success.
|
||||
A finite interval alone does not establish eligibility.
|
||||
|
||||
Preflight does not repeat bufferization or allocation.
|
||||
The backend owns final legality and signed 32-bit local-memory address validation.
|
||||
Resource failures marked by that owner permit an explicit retry from the original checkpoint.
|
||||
Unclassified failures stop compilation. Every attempted schedule and cause appears in the selection report.
|
||||
|
||||
### Complete-state recurrence
|
||||
|
||||
Queue analysis checks translated states at one fixed core's body entry.
|
||||
Snapshots contain completion counts relative to core entry, ready-input counts, and busy state for every event.
|
||||
Run-length encoding keeps these records compact.
|
||||
Snapshots also contain pending events in arbitration order, remaining FIFO tokens, controller availability, and the tie policy.
|
||||
One pending firing per event makes node IDs sufficient to resolve simultaneous-event ties.
|
||||
The graph and target parameters remain fixed within each query.
|
||||
Time offsets use checked unsigned differences.
|
||||
Equal states require equal positive generation progress on every core.
|
||||
This proves recurrence for the deterministic abstract model, not physical execution-time accuracy.
|
||||
Reporting counters and analysis budgets are not hardware state.
|
||||
|
||||
Token initialization distances and graph edges remain immutable within a query.
|
||||
Consumption clears a drained queue and resets its head immediately.
|
||||
Drained and never-populated queues therefore have the same readiness state.
|
||||
Consumed prefixes, capacities, and compaction indices cannot affect future timing or FIFO contents.
|
||||
An idle controller's past completion cannot delay future arrivals.
|
||||
These normalizations require causal, monotonic transitions, which the proof checks.
|
||||
|
||||
Proof construction uses at most 256 generations and 16 MiB of snapshot storage.
|
||||
Each checkpoint scans events, dependencies, and remaining tokens, then orders a copy of the pending frontier.
|
||||
Exact comparisons use bounded history; hashes only filter comparisons.
|
||||
Unproved recurrence remains explicit and cannot guide multi-candidate selection.
|
||||
When recurrence is proved, its interval is authoritative for that arbitration policy and its report.
|
||||
Proof-mode reports retain interval-window results under `legacy_interval_window`.
|
||||
Normal costing stops once recurrence is proved. Detailed diagnostics can continue to collect interval-window evidence.
|
||||
A legacy event or generation limit cannot override a later complete-state proof.
|
||||
Budget limits, lack of ready events, and unproved recurrence remain separate evidence.
|
||||
Internal diagnostic queries can cap interval-window work without reducing the recurrence proof budget.
|
||||
The standalone instruction diagnostic keeps its interval-window behavior when recurrence is not requested.
|
||||
|
||||
### Approximation and diagnostics
|
||||
|
||||
Scalar setup, copy forwarding, local reuse, and host-slice folding can change request readiness.
|
||||
These effects remain prediction limitations. Physical address reuse itself adds no execution work.
|
||||
Direct returns use the same return-operand test as lowering and preserve output multiplicity.
|
||||
Captured weights used by vector operations require host loads.
|
||||
Publication-only slices of compact results use `CopyAccessPlan` runs without an intermediate copy.
|
||||
Matrix writeback requires complete aligned array groups.
|
||||
Partial output groups, encoded layouts, unavailable geometry, and indirect output collection produce unavailable costs.
|
||||
|
||||
`--pim-periodic-diagnostics` adds resource summaries and ordered-request fingerprints.
|
||||
Normal compilation creates no fingerprints, detailed traces, or sensitivity runs.
|
||||
Optional local-copy sensitivity removes modeled copy work without changing generated code.
|
||||
It does not describe a legal transformed program or an interval bound.
|
||||
The standalone instruction diagnostic remains available for offline timing validation.
|
||||
|
||||
Checked arithmetic rejects unrepresentable sizes, loop bounds, repeated work, and service durations.
|
||||
Work-only repeats remain compact. Immutable summaries are cached within a query.
|
||||
Ordered memory and communication occurrences expand within the event budget.
|
||||
For `P` compact records, `R` expanded events, and `D` dependencies, construction costs `O(P+R+D)` time.
|
||||
With `B` processed events and `T` token propagations, queue evaluation costs `O(B log R+T)` time.
|
||||
Storage costs `O(P+R+G D)` for bounded generation count `G`, plus bounded recurrence history.
|
||||
Reports separate extraction, expansion, and queue-analysis times.
|
||||
Fingerprints compare memory and blocking-operation order, without addresses or work between requests.
|
||||
Equal fingerprints therefore do not prove equal request readiness or timing.
|
||||
|
||||
+73
-103
@@ -136,7 +136,8 @@ introduced after deferred realization, including final output collection, is
|
||||
also absent. Instruction issue, local contention, network injection queues, and
|
||||
memory arbitration remain unmodeled. Missing effects are listed in the report;
|
||||
`selection_eligible` remains false. This plan-level diagnostic does not rank
|
||||
production candidates. Production uses the instruction-level queue model below.
|
||||
production candidates. Production extracts execution events from realized Spatial
|
||||
and evaluates them with the shared queue engine.
|
||||
|
||||
For `V` events, `E` edges, and `R` recurrence edges, construction requires
|
||||
`O(T + F + S log S)` time and `O(V + E + S)` storage, for existing compute
|
||||
@@ -236,7 +237,8 @@ generations. The maximum request wait includes bootstrap. Event-index order
|
||||
resolves simultaneous readiness. A second run reverses that order and reports
|
||||
`alternative_tie_interval_ns`. These two orders are sensitivity checks, not
|
||||
bounds over every possible tie order. Clock phases and frontend overlap remain
|
||||
approximate. The standalone report remains read-only. Production selection uses the same queue model through its C++ API.
|
||||
approximate. The standalone report remains read-only.
|
||||
Production evaluates Spatial-derived events with the shared queue engine.
|
||||
|
||||
For `R` global requests, the queue mode adds `O(R)` events and dependencies.
|
||||
For `V` resulting events, `E` dependencies, and generation bound `K`, execution
|
||||
@@ -250,120 +252,88 @@ The standalone frontend retains the recurrence witness. Neither analysis changes
|
||||
|
||||
## Default throughput selection
|
||||
|
||||
Full PIM code generation performs these steps for `--pipeline > 1`.
|
||||
`--pim-only-codegen` retains the physical schedule already present in its input.
|
||||
Full PIM code generation uses Spatial-only queue-aware selection for `--pipeline > 1`.
|
||||
Latency PEFT and `--pim-only-codegen` retain their existing behavior.
|
||||
Intermediate emission retains the original schedule and stops at its requested stage.
|
||||
Its schedule can differ from the final program.
|
||||
|
||||
The normal compilation workflow is:
|
||||
The compiler performs these steps:
|
||||
|
||||
1. Run the ONNX frontend, layout planning, common lowering, and graph preparation once.
|
||||
2. Generate at most eleven verified physical schedules from the existing searches.
|
||||
3. Remove duplicate placements, including their core order and stage identities.
|
||||
4. Clone the prepared module for each candidate using MLIR's lossless `IRMapping`.
|
||||
5. Remap each schedule's compute identities to the clone.
|
||||
6. Realize communication and lower the clone through verification and instruction selection.
|
||||
7. Generate binary instruction records in memory, without weights, exports, or candidate files.
|
||||
8. Run both static controller-arbitration tie policies on that instruction program.
|
||||
9. Select an eligible candidate and emit only its normal program.
|
||||
1. Run the frontend, layout planning, and graph preparation once.
|
||||
2. Generate at most eleven legal schedules from the existing searches.
|
||||
3. Remove duplicate physical placements, including stage identities and core order.
|
||||
4. Realize each candidate in an independent clone of the prepared checkpoint.
|
||||
5. Query the target adapter over realized Spatial IR, then release the clone.
|
||||
6. Require supported static forms, available costs, and proved modeled recurrence for every candidate.
|
||||
7. Select a candidate with the existing 2.1% selection margin.
|
||||
8. Realize the selected schedule again from the unchanged checkpoint.
|
||||
9. Lower, allocate, verify, and emit only the selected program.
|
||||
|
||||
Immutable attributes, including constants and weights, remain shared in the MLIR
|
||||
context. Candidate operations, values, communication plans, and synchronization
|
||||
state remain independent. Trials never mutate the prepared checkpoint. Candidates
|
||||
run sequentially. At most one best lowered module survives between trials.
|
||||
The retained module follows the conservative selection rule throughout evaluation.
|
||||
A close alternative does not evict candidate zero. Unsupported timing contracts
|
||||
and synchronization ablation generate only the original schedule and lower it
|
||||
once. The compiler checks this before collecting alternatives. The frontend is
|
||||
not repeated.
|
||||
A single candidate skips event extraction, queue analysis, recurrence proof, and comparison.
|
||||
It is realized and lowered once, even when optional timing is unavailable.
|
||||
No rejected candidate enters PIM IR or instruction generation during cost evaluation.
|
||||
Constants and weights remain shared immutable attributes in the MLIR context.
|
||||
Candidate operations, communication plans, and synchronization state remain independent.
|
||||
No full candidate compilation directories are created.
|
||||
|
||||
Intermediate targets (`--EmitSpatial`, `--EmitPim`, and `--EmitPimBufferized`)
|
||||
retain the original schedule. They do not perform queue-aware selection or run
|
||||
transformations beyond the requested stage. Their schedule can therefore differ
|
||||
from final code generation. Full PIM code generation uses queue-aware selection.
|
||||
`SchedulingTarget::estimateRealizedScheduleCost` is the read-only whole-schedule query.
|
||||
Its typed result carries both intervals, recurrence evidence, static lowering support,
|
||||
failure reasons, and the selection margin. Unknown support remains explicit.
|
||||
The target supplies model evidence. Spatial applies the 2.1% selection policy margin.
|
||||
Selection does not read JSON diagnostics. Reports serialize the typed results.
|
||||
Spatial owns candidate generation, legality, placement, and target-neutral comparison.
|
||||
The PIM adapter owns execution-event extraction, instruction timing, and resource contention.
|
||||
The shared queue engine models controller arrival arbitration and cross-generation dependencies.
|
||||
The compiler does not execute candidate programs or invoke a simulator.
|
||||
|
||||
The PIM compiler owns instruction timing and controller behavior. Spatial owns
|
||||
legal candidate generation and schedule identity remapping. `PimPeriodicAnalysis`
|
||||
is the single instruction-level timing implementation. `raptor-periodic` is its
|
||||
read-only diagnostic frontend. The compiler invokes neither that executable nor
|
||||
an external selection script.
|
||||
Both arbitration policies must establish complete-state recurrence within bounded analysis budgets.
|
||||
Their estimates are sensitivity checks, not formal bounds.
|
||||
The candidate with the lowest larger estimate must separate from the original schedule.
|
||||
Comparison uses `[min(ties)/1.021, max(ties)/0.979]`.
|
||||
Close alternatives retain candidate zero. The 2.1% margin is a policy heuristic.
|
||||
Spatial timing predictions have no formal accuracy bound.
|
||||
The search finds the best distinguishable candidate in its bounded set, without global optimality.
|
||||
|
||||
`SchedulingTarget::prepareScheduleCost` is an optional whole-schedule cost query.
|
||||
It accepts a verified target-lowered module and returns an opaque program identity
|
||||
and a deferred static evaluation. Spatial compares the resulting interval
|
||||
estimates and applies the target's reported uncertainty. Missing uncertainty
|
||||
makes an estimate ineligible. The target owns instruction extraction,
|
||||
protocol timing, contention, and unavailable-result reasons. Preparation and
|
||||
evaluation do not change the candidate. Equal program identities avoid repeated
|
||||
queue analysis. An absent query retains the original schedule.
|
||||
If generation yields one distinct schedule, no comparison is required. The
|
||||
compiler realizes and lowers it once, without a trial clone, duplicate
|
||||
instruction generation, or queue analysis. The selection report records this
|
||||
case separately from unavailable estimates. Final code generation is unchanged.
|
||||
Unavailable timing, unknown static support, or unproved recurrence stops a multi-candidate comparison.
|
||||
The diagnostic identifies each affected candidate and property.
|
||||
The compiler does not silently exclude unknown candidates or use instruction-level fallback selection.
|
||||
|
||||
PIM transport details reside in `PimInstructionTiming`. The optional plan-level
|
||||
diagnostic also belongs to the PIM compiler. Spatial supplies its physical plans
|
||||
through an injected reporting callback; it does not encode the PIM handshake.
|
||||
The instruction emitter remains the source of register setup, address-dependent
|
||||
instruction suppression, loop expansion, and physical array-group identity.
|
||||
Candidate evaluation uses this emitter in memory after local-memory planning.
|
||||
It does not reconstruct these rules from scalar Spatial operation costs.
|
||||
Static preflight does not prove final backend legality.
|
||||
The selected candidate runs through normal bufferization, allocation, and backend verification.
|
||||
A backend owner can mark a resource-limit diagnostic as candidate-specific.
|
||||
Currently, the local-memory allocator marks signed 32-bit address-space exhaustion.
|
||||
The compiler discards that failed clone, explicitly records its rejection, and selects again from the remaining estimated schedules.
|
||||
The first remaining schedule becomes the reference for the same selection policy.
|
||||
Each schedule is attempted at most once. No graph or placement constraints are relaxed.
|
||||
Unclassified or systemic backend failures stop compilation immediately.
|
||||
Code-generation failures also stop compilation when recovery cannot be justified.
|
||||
If every candidate fails, one diagnostic lists all attempted candidates and causes.
|
||||
|
||||
Production projects native fp32 transfer sizes and vector widths to the existing
|
||||
oracle int8 timing contract. It preserves instruction counts, matrix precision,
|
||||
vector element counts, synchronization, and transfer identities. This projection
|
||||
does not change the emitted native program. Unsupported widths, targets without
|
||||
complete timing, and unsupported instruction semantics produce explicit fallback
|
||||
reasons. Synchronization ablation retains the original schedule.
|
||||
|
||||
Both arbitration tie runs must converge within the existing generation and event
|
||||
budgets. Their estimates are sensitivity checks, not formal bounds. Duplicate
|
||||
instruction programs are analyzed once. The candidate with the lowest larger
|
||||
estimate must separate from the original schedule. Close alternatives do not
|
||||
cancel a gain that separates from the original. Comparison uses `[min(ties)/1.021, max(ties)/0.979]`. The fixed 2.1% prediction uncertainty
|
||||
covers offline validation error with upward rounding. It is a selection margin,
|
||||
not a fitted instruction latency or a guaranteed error bound. Overlapping ranges
|
||||
with the original retain candidate zero. Missing original estimates also retain candidate zero.
|
||||
Unavailable alternatives cannot displace an eligible original. This search finds
|
||||
the best distinguishable candidate in its bounded set; it does not establish
|
||||
global optimality.
|
||||
|
||||
`reports/pipeline_selection.json` records candidate identities, estimates,
|
||||
selection reasons, and generation, lowering, instruction-generation, queue-analysis,
|
||||
and finalization times. The selected verified lowered module and its captured
|
||||
schedule report are reused without repeating materialization or communication
|
||||
realization. Memory reports come from normal final code generation.
|
||||
With `--enable-timing`, candidate summaries also contain per-pass lowering times
|
||||
and operation/value counts before bufferization and instruction generation.
|
||||
Instruction-generation summaries separate preparation from per-core emission.
|
||||
Explicit Spatial exports, IR snapshots,
|
||||
and plan-level periodic diagnostics reconstruct the selected Spatial schedule
|
||||
because those outputs require pre-lowering operations. This optional work does
|
||||
not change the retained instruction program. Candidate instructions and rejected
|
||||
IR are released immediately. No temporary compilation directories are created.
|
||||
`--pim-dump-ir` explicitly enables diagnostic IR snapshots;
|
||||
these snapshots elide large constants and cannot serve as lossless checkpoints.
|
||||
`reports/pipeline_selection.json` records Spatial costs, eligibility, selection, and backend attempts.
|
||||
It also records candidate generation, realization, extraction, expansion, queue, lowering, and finalization times.
|
||||
`--enable-timing` adds per-pass lowering times and IR operation/value counts.
|
||||
Final instruction generation uses the normal compiler timing report.
|
||||
Explicit Spatial exports and IR dumps reconstruct pre-lowering artifacts for the selected schedule.
|
||||
They do not change the emitted program. Diagnostic MLIR dumps elide large constants and are not lossless checkpoints.
|
||||
|
||||
### Static analysis, diagnostics, and external validation
|
||||
|
||||
Production decisions use only the input program, compiler options, candidate
|
||||
instructions and plans, and configured target resources and timing. The static
|
||||
model executes deterministic abstract events. It does not execute the generated
|
||||
program. It reads no benchmark CSV, profiling trace, measured latency cache, or
|
||||
reference artifact. Identical static inputs produce the same decision regardless
|
||||
of historical measurements. Throughput deferred-demand ordering uses IR order,
|
||||
rather than allocation addresses. Latency PEFT and its demand ordering are unchanged.
|
||||
Production decisions use the input graph, compiler options, physical plans, and configured target parameters only.
|
||||
Identical static inputs produce the same decision regardless of historical measurements.
|
||||
There are no measured-latency caches, workload lookup tables, or fitted timing corrections.
|
||||
|
||||
Optional diagnostics expose recurrence witnesses, instruction timing, and resource
|
||||
service. Offline Pimsim fast-mode measurements validate these predictions and
|
||||
candidate rankings. Simulator results never enter production selection. Timing
|
||||
corrections must follow verified hardware behavior or configured parameters.
|
||||
`raptor-periodic` retains the instruction-derived analyzer as a standalone validation frontend.
|
||||
It shares the same timing definitions and queue engine with Spatial event evaluation.
|
||||
Optional diagnostics expose resource work, recurrence, and ordered-request fingerprints.
|
||||
Offline Pimsim fast-mode runs validate predictions and emitted programs.
|
||||
Simulator measurements never enter production selection.
|
||||
|
||||
For `B <= 11` schedules, `T` compute instances, and `P` cores, captured schedules
|
||||
use `O(B(T+P))` storage. Placement deduplication takes `O(B²T)` time. Each distinct
|
||||
finalist requires downstream lowering and one instruction scan. Queue execution
|
||||
uses the bounded complexity stated above. Peak storage includes the prepared
|
||||
module, one best lowered module, one trial, and one queue graph. It does not scale
|
||||
with retained complete candidate programs. Maximum-cycle-ratio diagnostics do not
|
||||
run in the candidate evaluation loop.
|
||||
For `C <= 11` candidates, scheduling metadata uses `O(C(T+P))` storage for `T` compute instances and `P` cores.
|
||||
Placement deduplication costs `O(C²T)` time.
|
||||
Candidate realization and extraction are sequential; complete candidate modules are not retained.
|
||||
Normal successful compilation performs one candidate-specific backend lowering.
|
||||
Recovery performs at most `C` lowerings. Retry selection costs `O(C²)` time and `O(C)` metadata.
|
||||
The bounded queue-analysis costs are described in the implementation notes.
|
||||
|
||||
## Capacity and recovery
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ add_pim_library(OMPimCommon
|
||||
IR/AddressAnalysis.cpp
|
||||
IR/BatchCoreUtils.cpp
|
||||
IR/ConstantUtils.cpp
|
||||
IR/CopyAccessPlan.cpp
|
||||
IR/CoreBlockUtils.cpp
|
||||
IR/EntryPointUtils.cpp
|
||||
IR/IndexingUtils.cpp
|
||||
|
||||
@@ -171,7 +171,9 @@ static llvm::FailureOr<int64_t> resolveConstantGlobalLoad(mlir::memref::LoadOp l
|
||||
if (indices.size() != static_cast<size_t>(globalType.getRank()))
|
||||
return mlir::failure();
|
||||
|
||||
auto strides = computeRowMajorStrides(globalType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(globalType.getShape());
|
||||
if (failed(checkedStrides)) return mlir::failure();
|
||||
auto strides = std::move(*checkedStrides);
|
||||
int64_t linearIndex = linearizeIndex(indices, strides);
|
||||
if (linearIndex < 0 || linearIndex >= globalType.getNumElements())
|
||||
return mlir::failure();
|
||||
@@ -308,7 +310,9 @@ llvm::FailureOr<CompiledIndexExpr> compileConstantGlobalLoad(mlir::memref::LoadO
|
||||
CompiledIndexExprNode expr;
|
||||
expr.kind = CompiledIndexExprNode::Kind::ConstantGlobalLoad;
|
||||
expr.globalOp = globalOp;
|
||||
expr.globalStrides = computeRowMajorStrides(globalType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(globalType.getShape());
|
||||
if (failed(checkedStrides)) return mlir::failure();
|
||||
expr.globalStrides = std::move(*checkedStrides);
|
||||
expr.operands.reserve(loadOp.getIndices().size());
|
||||
for (mlir::Value index : loadOp.getIndices()) {
|
||||
auto compiledIndex = compileIndexValueImpl(index);
|
||||
|
||||
@@ -10,6 +10,18 @@ using namespace mlir;
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
bool isZeroBitPatternSplat(Attribute value) {
|
||||
auto dense = dyn_cast_or_null<DenseElementsAttr>(value);
|
||||
if (!dense || !dense.isSplat())
|
||||
return false;
|
||||
Attribute element = dense.getSplatValue<Attribute>();
|
||||
if (auto floating = dyn_cast<FloatAttr>(element))
|
||||
return floating.getValue().isZero() && !floating.getValue().isNegative();
|
||||
if (auto integer = dyn_cast<IntegerAttr>(element))
|
||||
return integer.getValue().isZero();
|
||||
return false;
|
||||
}
|
||||
|
||||
ConstantPool::ConstantPool(Operation *constantAnchor, OpBuilder &builder)
|
||||
: anchor(constantAnchor), block(getConstantInsertionBlock(constantAnchor)),
|
||||
builder(builder) {
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
// Negative floating zero has a distinct representation and must not match.
|
||||
bool isZeroBitPatternSplat(mlir::Attribute value);
|
||||
|
||||
class ConstantPool {
|
||||
public:
|
||||
ConstantPool(mlir::Operation *constantAnchor, mlir::OpBuilder &builder);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "CopyAccessPlan.hpp"
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
#include "llvm/Support/CheckedArithmetic.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
mlir::FailureOr<int64_t> getContiguousSuffixRank(
|
||||
llvm::ArrayRef<int64_t> shape, llvm::ArrayRef<int64_t> elementStrides) {
|
||||
if (shape.size() != elementStrides.size()
|
||||
|| llvm::any_of(shape, [](int64_t size) { return size <= 0; }))
|
||||
return mlir::failure();
|
||||
int64_t expectedStride = 1, rank = 0;
|
||||
for (size_t dim = shape.size(); dim-- > 0;) {
|
||||
if (shape[dim] != 1 && elementStrides[dim] != expectedStride)
|
||||
break;
|
||||
++rank;
|
||||
auto next = llvm::checkedMul(expectedStride, shape[dim]);
|
||||
if (!next) return mlir::failure();
|
||||
expectedStride = *next;
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
|
||||
mlir::FailureOr<CopyAccessPlan> CopyAccessPlan::build(
|
||||
llvm::ArrayRef<int64_t> shape, llvm::ArrayRef<int64_t> sourceElementStrides,
|
||||
llvm::ArrayRef<int64_t> targetElementStrides) {
|
||||
auto sourceSuffix = getContiguousSuffixRank(shape, sourceElementStrides);
|
||||
auto targetSuffix = getContiguousSuffixRank(shape, targetElementStrides);
|
||||
if (mlir::failed(sourceSuffix) || mlir::failed(targetSuffix))
|
||||
return mlir::failure();
|
||||
size_t outerRank = shape.size() - std::min(*sourceSuffix, *targetSuffix);
|
||||
CopyAccessPlan plan;
|
||||
plan.outerShape.assign(shape.begin(), shape.begin() + outerRank);
|
||||
plan.sourceOuterStrides.assign(sourceElementStrides.begin(), sourceElementStrides.begin() + outerRank);
|
||||
plan.targetOuterStrides.assign(targetElementStrides.begin(), targetElementStrides.begin() + outerRank);
|
||||
int64_t elements = 1, maximumSource = 0, maximumTarget = 0;
|
||||
for (size_t dim = 0; dim < shape.size(); ++dim) {
|
||||
if (dim < outerRank && (sourceElementStrides[dim] < 0 || targetElementStrides[dim] < 0))
|
||||
return mlir::failure();
|
||||
int64_t &product = dim < outerRank ? plan.runCount : plan.runElements;
|
||||
auto next = llvm::checkedMul(product, shape[dim]);
|
||||
auto total = llvm::checkedMul(elements, shape[dim]);
|
||||
if (!next || !total) return mlir::failure();
|
||||
product = *next;
|
||||
elements = *total;
|
||||
// Unit dimensions never access their stride, including negative strides.
|
||||
if (shape[dim] != 1) {
|
||||
auto sourceEnd = llvm::checkedMulAdd(shape[dim] - 1, sourceElementStrides[dim], maximumSource);
|
||||
auto targetEnd = llvm::checkedMulAdd(shape[dim] - 1, targetElementStrides[dim], maximumTarget);
|
||||
if (!sourceEnd || !targetEnd) return mlir::failure();
|
||||
maximumSource = *sourceEnd;
|
||||
maximumTarget = *targetEnd;
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
mlir::FailureOr<std::pair<int64_t, int64_t>> CopyAccessPlan::getRunOffsets(int64_t run) const {
|
||||
if (run < 0 || run >= runCount) return mlir::failure();
|
||||
int64_t source = 0, target = 0;
|
||||
for (size_t dim = outerShape.size(); dim-- > 0;) {
|
||||
int64_t coordinate = run % outerShape[dim];
|
||||
run /= outerShape[dim];
|
||||
auto nextSource = llvm::checkedMulAdd(coordinate, sourceOuterStrides[dim], source);
|
||||
auto nextTarget = llvm::checkedMulAdd(coordinate, targetOuterStrides[dim], target);
|
||||
if (!nextSource || !nextTarget) return mlir::failure();
|
||||
source = *nextSource;
|
||||
target = *nextTarget;
|
||||
}
|
||||
return std::pair{source, target};
|
||||
}
|
||||
|
||||
} // namespace onnx_mlir
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "mlir/Support/LogicalResult.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
// Geometry only. Callers must prove storage layout, aliasing, and copy legality.
|
||||
mlir::FailureOr<int64_t> getContiguousSuffixRank(
|
||||
llvm::ArrayRef<int64_t> shape, llvm::ArrayRef<int64_t> elementStrides);
|
||||
|
||||
class CopyAccessPlan {
|
||||
public:
|
||||
// All accessed relative element offsets must fit int64_t, including run tails.
|
||||
// Byte scaling and addition of base addresses remain caller responsibilities.
|
||||
static mlir::FailureOr<CopyAccessPlan> build(llvm::ArrayRef<int64_t> shape,
|
||||
llvm::ArrayRef<int64_t> sourceElementStrides,
|
||||
llvm::ArrayRef<int64_t> targetElementStrides);
|
||||
|
||||
int64_t getRunCount() const { return runCount; }
|
||||
int64_t getRunElements() const { return runElements; }
|
||||
llvm::ArrayRef<int64_t> getOuterShape() const { return outerShape; }
|
||||
llvm::ArrayRef<int64_t> getSourceOuterStrides() const { return sourceOuterStrides; }
|
||||
llvm::ArrayRef<int64_t> getTargetOuterStrides() const { return targetOuterStrides; }
|
||||
// Source and target element offsets, relative to their separately proven bases.
|
||||
mlir::FailureOr<std::pair<int64_t, int64_t>> getRunOffsets(int64_t run) const;
|
||||
|
||||
private:
|
||||
int64_t runCount = 1;
|
||||
int64_t runElements = 1;
|
||||
llvm::SmallVector<int64_t> outerShape, sourceOuterStrides, targetOuterStrides;
|
||||
};
|
||||
|
||||
} // namespace onnx_mlir
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/Support/ErrorHandling.h"
|
||||
#include "llvm/Support/CheckedArithmetic.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
@@ -9,9 +10,20 @@
|
||||
namespace onnx_mlir {
|
||||
|
||||
llvm::SmallVector<int64_t> computeRowMajorStrides(llvm::ArrayRef<int64_t> shape) {
|
||||
auto strides = computeRowMajorStridesChecked(shape);
|
||||
if (mlir::failed(strides))
|
||||
llvm::report_fatal_error("row-major strides are not representable in int64_t");
|
||||
return std::move(*strides);
|
||||
}
|
||||
|
||||
mlir::FailureOr<llvm::SmallVector<int64_t>>
|
||||
computeRowMajorStridesChecked(llvm::ArrayRef<int64_t> shape) {
|
||||
llvm::SmallVector<int64_t> strides(shape.size(), 1);
|
||||
for (int64_t dim = static_cast<int64_t>(shape.size()) - 2; dim >= 0; --dim)
|
||||
strides[dim] = strides[dim + 1] * shape[dim + 1];
|
||||
for (int64_t dim = static_cast<int64_t>(shape.size()) - 2; dim >= 0; --dim) {
|
||||
auto stride = llvm::checkedMul(strides[dim + 1], shape[dim + 1]);
|
||||
if (!stride) return mlir::failure();
|
||||
strides[dim] = *stride;
|
||||
}
|
||||
return strides;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ namespace onnx_mlir {
|
||||
using HSliceId = size_t;
|
||||
using CoreId = size_t;
|
||||
|
||||
// Invariant form fails explicitly on overflow, including assertion-free builds.
|
||||
// Optional analyses and rewrites must use the checked form.
|
||||
llvm::SmallVector<int64_t> computeRowMajorStrides(llvm::ArrayRef<int64_t> shape);
|
||||
mlir::FailureOr<llvm::SmallVector<int64_t>>
|
||||
computeRowMajorStridesChecked(llvm::ArrayRef<int64_t> shape);
|
||||
|
||||
struct TransposeRun {
|
||||
size_t start;
|
||||
|
||||
@@ -191,7 +191,9 @@ resolveWeightView(mlir::Operation* coreLikeOp, mlir::Value weight, const StaticV
|
||||
ResolvedWeightView view;
|
||||
view.globalOp = globalOp;
|
||||
view.shape.assign(denseAttr.getType().getShape().begin(), denseAttr.getType().getShape().end());
|
||||
view.strides = computeRowMajorStrides(view.shape);
|
||||
auto checkedStrides = computeRowMajorStridesChecked(view.shape);
|
||||
if (failed(checkedStrides)) return mlir::failure();
|
||||
view.strides = std::move(*checkedStrides);
|
||||
|
||||
CompiledIndexExpr offsetExpr = makeConstantExpr(0);
|
||||
for (mlir::Operation* viewOp : llvm::reverse(viewOps)) {
|
||||
|
||||
@@ -4,6 +4,21 @@
|
||||
|
||||
namespace onnx_mlir::pim {
|
||||
|
||||
static constexpr llvm::StringLiteral candidateResourceFailure = "pim.candidate-resource-limit";
|
||||
|
||||
mlir::InFlightDiagnostic emitCandidateResourceError(mlir::Operation *op, llvm::StringRef message) {
|
||||
auto diagnostic = op->emitError(message);
|
||||
diagnostic.getUnderlyingDiagnostic()->getMetadata().emplace_back(llvm::StringRef(candidateResourceFailure));
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
bool isCandidateResourceError(mlir::Diagnostic &diagnostic) {
|
||||
return llvm::any_of(diagnostic.getMetadata(), [](const mlir::DiagnosticArgument &argument) {
|
||||
return argument.getKind() == mlir::DiagnosticArgument::DiagnosticArgumentKind::String
|
||||
&& argument.getAsString() == candidateResourceFailure;
|
||||
});
|
||||
}
|
||||
|
||||
mlir::InFlightDiagnostic emitUnsupportedStaticShapeDiagnostic(mlir::Operation* op, llvm::StringRef valueDescription) {
|
||||
return op->emitOpError() << "requires statically shaped " << valueDescription;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
namespace onnx_mlir::pim {
|
||||
|
||||
/// Only a backend owner can classify a failed schedule as resource-infeasible.
|
||||
mlir::InFlightDiagnostic emitCandidateResourceError(mlir::Operation *op, llvm::StringRef message);
|
||||
bool isCandidateResourceError(mlir::Diagnostic &diagnostic);
|
||||
|
||||
struct CappedDiagnosticReporter {
|
||||
explicit CappedDiagnosticReporter(int64_t maxReportedFailures = 8)
|
||||
: maxReportedFailures(maxReportedFailures) {}
|
||||
|
||||
@@ -17,6 +17,8 @@ add_pim_library(OMPimCompilerUtils
|
||||
PimCompilerUtils.cpp
|
||||
PimScheduleDiagnostic.cpp
|
||||
PimPeriodicAnalysis.cpp
|
||||
PimQueueReporting.cpp
|
||||
PimSpatialCost.cpp
|
||||
PimPipelineSelection.cpp
|
||||
PimArtifactWriter.cpp
|
||||
PimCodeGen.cpp
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace onnx_mlir::pim_binary {
|
||||
|
||||
inline constexpr char kMagic[4] = {'P', 'I', 'M', 'B'};
|
||||
inline constexpr uint32_t kVersion = 1;
|
||||
inline constexpr int32_t kMatrixWeightPrecisionBits = 8;
|
||||
inline constexpr uint64_t kCountOffset = 8;
|
||||
inline constexpr size_t kHeaderSize = 12;
|
||||
inline constexpr size_t kRecordSize = 20;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <absl/types/compare.h>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
@@ -32,6 +31,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "Common/IR/CompactAsmUtils.hpp"
|
||||
#include "Common/IR/ConstantUtils.hpp"
|
||||
#include "Common/PimCommon.hpp"
|
||||
#include "Common/Support/CheckedArithmetic.hpp"
|
||||
#include "Common/Support/Diagnostics.hpp"
|
||||
@@ -42,7 +42,6 @@
|
||||
#include "src/Accelerators/PIM/Common/Support/FileSystemUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimArtifactWriter.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimBinaryFormat.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimPeriodicAnalysis.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCodeGen.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCoreProgram.hpp"
|
||||
@@ -116,15 +115,7 @@ static bool isZeroSplatGlobal(mlir::Value value) {
|
||||
auto globalOp = moduleOp ? lookupGlobalForGetGlobal(moduleOp, getGlobalOp) : memref::GlobalOp();
|
||||
if (!globalOp || !globalOp.getConstant() || !globalOp.getInitialValue())
|
||||
return false;
|
||||
auto denseAttr = dyn_cast<DenseElementsAttr>(*globalOp.getInitialValue());
|
||||
if (!denseAttr || !denseAttr.isSplat())
|
||||
return false;
|
||||
Attribute valueAttr = denseAttr.getSplatValue<Attribute>();
|
||||
if (auto floatAttr = dyn_cast<FloatAttr>(valueAttr))
|
||||
return floatAttr.getValue().isZero() && !floatAttr.getValue().isNegative();
|
||||
if (auto integerAttr = dyn_cast<IntegerAttr>(valueAttr))
|
||||
return integerAttr.getValue().isZero();
|
||||
return false;
|
||||
return isZeroBitPatternSplat(*globalOp.getInitialValue());
|
||||
}
|
||||
|
||||
// Pim instruction immediates are serialized as signed int32_t fields today
|
||||
@@ -613,7 +604,7 @@ void PimCodeGen::emitMvmOp(size_t groupId, size_t rdAddr, size_t rdOffset, size_
|
||||
instruction.opcode = pim_binary::Opcode::mvmul;
|
||||
instruction.rd = 0;
|
||||
instruction.r1 = 1;
|
||||
instruction.r2OrImm = 8;
|
||||
instruction.r2OrImm = pim_binary::kMatrixWeightPrecisionBits;
|
||||
instruction.generic1 = 0;
|
||||
instruction.generic2 = pim::checkedI32OrCrash(groupId, "mvm group id");
|
||||
emitInstruction(instruction);
|
||||
@@ -895,7 +886,6 @@ struct CoreEmissionResult {
|
||||
|
||||
OnnxMlirCompilerErrorCodes status = CompilerSuccess;
|
||||
MemoryReportRow reportRow;
|
||||
std::string instructions;
|
||||
llvm::SmallVector<ResolvedWeightView, 8> usedWeights;
|
||||
llvm::SmallVector<DiagnosticRecord, kMaxStoredCodegenDiagnostics> diagnostics;
|
||||
size_t diagnosticCount = 0;
|
||||
@@ -1150,10 +1140,8 @@ static OnnxMlirCompilerErrorCodes emitEmptyCoreArtifacts(StringRef outputDirPath
|
||||
return CompilerSuccess;
|
||||
}
|
||||
|
||||
OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::string& outputDirPath,
|
||||
PimInstructionProgram *instructions, PimCodegenTiming *timing) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
if (!instructions && !outputDirPath.empty()) {
|
||||
OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::string& outputDirPath) {
|
||||
if (!outputDirPath.empty()) {
|
||||
if (auto error = sys::fs::create_directory(outputDirPath)) {
|
||||
errs() << "Error creating output directory: " << outputDirPath << ": " << error.message() << '\n';
|
||||
return InvalidOutputFileAccess;
|
||||
@@ -1169,9 +1157,8 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
|
||||
memory.hostMem.allocateHost(moduleOp, funcOp);
|
||||
memory.reportHost();
|
||||
|
||||
if (!instructions)
|
||||
if (auto err = writeMemoryBinary(moduleOp, funcOp, memory, outputDirPath))
|
||||
return err;
|
||||
if (auto err = writeMemoryBinary(moduleOp, funcOp, memory, outputDirPath))
|
||||
return err;
|
||||
|
||||
json::Object xbarsPerArrayGroup;
|
||||
size_t maxCoreId = 0;
|
||||
@@ -1301,22 +1288,16 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
|
||||
|
||||
std::error_code errorCode;
|
||||
auto outputCorePath = outputDirPath + "/core_" + std::to_string(job.physicalCoreId) + ".pim";
|
||||
llvm::SmallVector<char, 0> binary;
|
||||
raw_svector_ostream memoryStream(binary);
|
||||
std::unique_ptr<raw_fd_ostream> fileStream;
|
||||
if (!instructions) {
|
||||
fileStream = std::make_unique<raw_fd_ostream>(outputCorePath, errorCode, sys::fs::OF_None);
|
||||
if (errorCode) {
|
||||
errs() << "Error opening core file: " << outputCorePath << ": " << errorCode.message() << '\n';
|
||||
result.status = InvalidOutputFileAccess;
|
||||
return result;
|
||||
}
|
||||
auto fileStream = std::make_unique<raw_fd_ostream>(outputCorePath, errorCode, sys::fs::OF_None);
|
||||
if (errorCode) {
|
||||
errs() << "Error opening core file: " << outputCorePath << ": " << errorCode.message() << '\n';
|
||||
result.status = InvalidOutputFileAccess;
|
||||
return result;
|
||||
}
|
||||
raw_pwrite_stream &coreBinaryStream = instructions
|
||||
? static_cast<raw_pwrite_stream &>(memoryStream) : *fileStream;
|
||||
raw_pwrite_stream &coreBinaryStream = *fileStream;
|
||||
|
||||
std::unique_ptr<raw_fd_ostream> coreJsonStream;
|
||||
if (!instructions && pimEmitJson.getValue()) {
|
||||
if (pimEmitJson.getValue()) {
|
||||
std::string outputCoreJsonPath = outputDirPath + "/core_" + std::to_string(job.physicalCoreId) + ".json";
|
||||
errorCode = std::error_code();
|
||||
coreJsonStream = std::make_unique<raw_fd_ostream>(outputCoreJsonPath, errorCode);
|
||||
@@ -1332,12 +1313,8 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
|
||||
PimCodeGen coreCodeGen(jobMemory, instructionWriter, coreJsonStream.get());
|
||||
auto finalizeInstructions = [&]() {
|
||||
bool succeeded = mlir::succeeded(instructionWriter.finalize());
|
||||
if (fileStream) {
|
||||
fileStream->close();
|
||||
if (fileStream->has_error()) { fileStream->clear_error(); succeeded = false; }
|
||||
} else {
|
||||
result.instructions.assign(binary.begin(), binary.end());
|
||||
}
|
||||
fileStream->close();
|
||||
if (fileStream->has_error()) { fileStream->clear_error(); succeeded = false; }
|
||||
return succeeded;
|
||||
};
|
||||
|
||||
@@ -1390,14 +1367,8 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
|
||||
};
|
||||
|
||||
std::vector<CoreEmissionResult> jobResults(jobs.size());
|
||||
auto emissionStart = std::chrono::steady_clock::now();
|
||||
if (timing)
|
||||
timing->preparationSeconds = std::chrono::duration<double>(emissionStart - start).count();
|
||||
mlir::parallelFor(
|
||||
moduleOp.getContext(), 0, jobs.size(), [&](size_t index) { jobResults[index] = emitJob(jobs[index]); });
|
||||
if (timing)
|
||||
timing->emissionSeconds = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - emissionStart).count();
|
||||
|
||||
pim::CappedDiagnosticReporter diagnostics;
|
||||
Operation* summaryAnchor = nullptr;
|
||||
@@ -1427,33 +1398,13 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
|
||||
physicalCoreId < activePhysicalCores.size(); ++physicalCoreId) {
|
||||
if (activePhysicalCores[physicalCoreId])
|
||||
continue;
|
||||
if (!instructions)
|
||||
if (auto err = emitEmptyCoreArtifacts(outputDirPath, physicalCoreId))
|
||||
return err;
|
||||
if (auto err = emitEmptyCoreArtifacts(outputDirPath, physicalCoreId))
|
||||
return err;
|
||||
xbarsPerArrayGroup["core" + std::to_string(physicalCoreId)] =
|
||||
json::Array {};
|
||||
memory.recordCoreReport(physicalCoreId, MemoryReportRow {});
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
instructions->instructions.resize(maxPhysicalCoreId + 1);
|
||||
instructions->arrayGroups.resize(maxPhysicalCoreId + 1);
|
||||
for (size_t core = 0; core <= maxPhysicalCoreId; ++core) {
|
||||
instructions->arrayGroups[core].clear();
|
||||
llvm::SmallVector<char> empty;
|
||||
raw_svector_ostream stream(empty);
|
||||
pim_binary::writeHeader(stream);
|
||||
instructions->instructions[core].assign(empty.begin(), empty.end());
|
||||
}
|
||||
for (size_t i = 0; i < jobs.size(); ++i) {
|
||||
size_t core = jobs[i].physicalCoreId;
|
||||
instructions->instructions[core] = std::move(jobResults[i].instructions);
|
||||
for (const auto &weight : jobResults[i].usedWeights)
|
||||
instructions->arrayGroups[core].push_back(weight.shape[1] / crossbarSize.getValue());
|
||||
}
|
||||
return CompilerSuccess;
|
||||
}
|
||||
|
||||
llvm::SmallVector<WeightFileRequest, 8> weightRequests;
|
||||
weightRequests.reserve(jobs.size());
|
||||
for (size_t jobIndex = 0; jobIndex < jobs.size(); ++jobIndex) {
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
namespace onnx_mlir {
|
||||
|
||||
struct CompiledCoreProgram;
|
||||
struct PimInstructionProgram;
|
||||
class PimInstructionWriter;
|
||||
|
||||
struct MemEntry {
|
||||
@@ -227,13 +226,7 @@ public:
|
||||
|
||||
};
|
||||
|
||||
struct PimCodegenTiming {
|
||||
double preparationSeconds = 0;
|
||||
double emissionSeconds = 0;
|
||||
};
|
||||
|
||||
OnnxMlirCompilerErrorCodes compileToPimCode(mlir::ModuleOp& moduleOpRef, std::string& outputDirName,
|
||||
PimInstructionProgram *instructions = nullptr, PimCodegenTiming *timing = nullptr);
|
||||
OnnxMlirCompilerErrorCodes compileToPimCode(mlir::ModuleOp& moduleOpRef, std::string& outputDirName);
|
||||
|
||||
} // namespace onnx_mlir
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ llvm::cl::opt<bool> pimReportConvLowering("pim-report-conv-lowering",
|
||||
llvm::cl::cat(OnnxMlirOptions));
|
||||
|
||||
llvm::cl::opt<bool> pimPeriodicDiagnostics("pim-periodic-diagnostics",
|
||||
llvm::cl::desc("Emit the optional pre-codegen recurrence timing report"),
|
||||
llvm::cl::desc("Emit optional Spatial recurrence and execution-work reports"),
|
||||
llvm::cl::init(false), llvm::cl::cat(OnnxMlirOptions));
|
||||
|
||||
llvm::cl::opt<bool> pimEmitJson("pim-emit-json",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
|
||||
#include "PimPipelineSelection.hpp"
|
||||
#include "PimInstructionTiming.hpp"
|
||||
#include "PimPeriodicAnalysis.hpp"
|
||||
#include "PimSpatialCost.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCompilerUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/ONNXToSpatialOptions.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
// Emptiness is normalized after consumption. Capacity and consumed prefixes
|
||||
// cannot affect virtual timing, readiness, or the remaining FIFO sequence.
|
||||
class PimDependencyTokens {
|
||||
llvm::SmallVector<uint64_t, 1> values;
|
||||
size_t head = 0;
|
||||
public:
|
||||
void initialize(size_t distance) { values.assign(distance, 0); head = 0; }
|
||||
bool empty() const { return values.empty(); }
|
||||
void push(uint64_t time) { values.push_back(time); }
|
||||
uint64_t consume() {
|
||||
assert(!empty());
|
||||
uint64_t time = values[head++];
|
||||
if (head == values.size()) { values.clear(); head = 0; }
|
||||
else if (head >= values.size() - head) {
|
||||
values.erase(values.begin(), values.begin() + head);
|
||||
head = 0;
|
||||
}
|
||||
return time;
|
||||
}
|
||||
llvm::ArrayRef<uint64_t> remaining() const { return llvm::ArrayRef(values).drop_front(head); }
|
||||
};
|
||||
|
||||
} // namespace onnx_mlir
|
||||
@@ -6,6 +6,7 @@ namespace onnx_mlir {
|
||||
|
||||
// PIM transport and instruction timing. Spatial consumes only the base costs.
|
||||
struct PimInstructionTiming : spatial::SchedulingTarget {
|
||||
Cost matrixCellBits = 0;
|
||||
bool peerPermissionThenSourceRead = false;
|
||||
Cost signalIssueNs = 0;
|
||||
Cost controlMessageBytes = 0;
|
||||
@@ -22,15 +23,17 @@ struct PimInstructionTiming : spatial::SchedulingTarget {
|
||||
&& localMemoryWidthBytes && transferWidthBytes;
|
||||
}
|
||||
|
||||
Cost getLinkServiceNs(Cost bytes, size_t source, size_t destination) const {
|
||||
Cost flits = bytes / transferWidthBytes + (bytes % transferWidthBytes != 0);
|
||||
return checkedMultiply(flits,
|
||||
template <typename Arithmetic = SchedulingCostArithmetic>
|
||||
Cost getLinkServiceNs(Cost bytes, size_t source, size_t destination, Arithmetic math = {}) const {
|
||||
Cost flits = math.divideUp(bytes, transferWidthBytes);
|
||||
return math.multiply(flits,
|
||||
getInterProcessorLatencyNs(source, destination));
|
||||
}
|
||||
|
||||
Cost getSharedMemoryLinkServiceNs(Cost bytes, size_t core, bool response) const {
|
||||
Cost flits = bytes / transferWidthBytes + (bytes % transferWidthBytes != 0);
|
||||
return checkedMultiply(flits, response ? sharedMemoryResponseLatencyNs[core]
|
||||
template <typename Arithmetic = SchedulingCostArithmetic>
|
||||
Cost getSharedMemoryLinkServiceNs(Cost bytes, size_t core, bool response, Arithmetic math = {}) const {
|
||||
Cost flits = math.divideUp(bytes, transferWidthBytes);
|
||||
return math.multiply(flits, response ? sharedMemoryResponseLatencyNs[core]
|
||||
: sharedMemoryRequestLatencyNs[core]);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "llvm/Support/JSON.h"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/SchedulingTarget.hpp"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace onnx_mlir {
|
||||
namespace spatial { struct SchedulingTarget; }
|
||||
struct PimInstructionTiming;
|
||||
void configurePimScheduleCosts(spatial::SchedulingTarget &target, PimInstructionTiming timing);
|
||||
|
||||
enum class PimQueueTermination {
|
||||
NotRun, IntervalConverged, RecurrenceProved, EventBudget, GenerationLimit,
|
||||
HistoryLimit, NonMonotonic, NoReadyEvents
|
||||
};
|
||||
|
||||
struct PimQueueEventLocation {
|
||||
int64_t core = 0;
|
||||
uint32_t pc = 0;
|
||||
std::string kind;
|
||||
};
|
||||
|
||||
struct PimQueueProgress {
|
||||
size_t processedEvents = 0, completedGenerations = 0;
|
||||
uint64_t maximumQueueWaitNs = 0;
|
||||
std::optional<PimQueueEventLocation> maximumQueueRequest, generationLimit;
|
||||
std::vector<std::pair<int64_t, double>> perCoreQueueWaitNs;
|
||||
std::vector<uint64_t> completedIntervalsNs;
|
||||
};
|
||||
|
||||
struct PimQueueIntervalWindow {
|
||||
std::optional<double> intervalNs;
|
||||
std::string reason;
|
||||
PimQueueTermination termination = PimQueueTermination::NotRun;
|
||||
PimQueueProgress progress;
|
||||
};
|
||||
|
||||
struct PimQueuePolicyResult {
|
||||
PimQueueIntervalWindow legacy;
|
||||
spatial::ScheduleRecurrenceEvidence recurrence;
|
||||
PimQueueProgress progress;
|
||||
PimQueueTermination termination = PimQueueTermination::NotRun;
|
||||
size_t checkpoints = 0, historyBytes = 0, stateWords = 0, proofProcessedEvents = 0;
|
||||
bool reverseTies = false;
|
||||
std::optional<double> intervalNs() const;
|
||||
};
|
||||
|
||||
struct PimQueueEvaluation {
|
||||
std::array<PimQueuePolicyResult, 2> policies;
|
||||
uint64_t memoryServiceNs = 0;
|
||||
double evaluationSeconds = 0;
|
||||
bool recurrenceRequired = false, details = false, measureTime = false;
|
||||
spatial::WholeScheduleCost cost() const;
|
||||
};
|
||||
|
||||
llvm::json::Object serializePimQueueEvaluation(const PimQueueEvaluation &evaluation);
|
||||
|
||||
struct PimPeriodicEstimate {
|
||||
spatial::WholeScheduleCost cost;
|
||||
llvm::json::Object report;
|
||||
std::optional<PimQueueEvaluation> queue;
|
||||
};
|
||||
|
||||
// Binary instructions, indexed by physical core. No executable or measured data.
|
||||
struct PimInstructionProgram {
|
||||
@@ -16,15 +67,58 @@ struct PimInstructionProgram {
|
||||
std::vector<std::vector<int64_t>> arrayGroups;
|
||||
};
|
||||
|
||||
// Hardware work only. Bytes are already in the target timing precision.
|
||||
// Repeat keeps ordered request patterns compact until queue construction.
|
||||
struct PimExecutionEvent {
|
||||
enum Kind { Work, Load, Store, Send, Receive, Signal, Wait, Repeat };
|
||||
enum WorkClass { Matrix, Vector, LocalCopy, Fill, VectorCopy, ScalarIssue };
|
||||
Kind kind = Work;
|
||||
uint64_t amount = 0;
|
||||
int64_t peer = 0, reg = 0, count = 0;
|
||||
std::vector<PimExecutionEvent> body;
|
||||
bool constant = false;
|
||||
WorkClass workClass = Fill;
|
||||
PimExecutionEvent(Kind kind, uint64_t amount, int64_t peer = 0,
|
||||
int64_t reg = 0, int64_t count = 0, std::vector<PimExecutionEvent> body = {})
|
||||
: kind(kind), amount(amount), peer(peer), reg(reg), count(count), body(std::move(body)) {}
|
||||
};
|
||||
using PimExecutionProgram = std::vector<std::vector<PimExecutionEvent>>;
|
||||
|
||||
struct PimPeriodicOptions {
|
||||
bool networkQueues = true;
|
||||
bool instructionIssue = true;
|
||||
bool scalarIssue = true;
|
||||
bool ignoreLocalCopies = false;
|
||||
bool workBreakdown = false;
|
||||
bool measureTime = false;
|
||||
// Optional ordered work/IO extraction for timing validation; no execution.
|
||||
PimExecutionProgram *executionTrace = nullptr;
|
||||
// Prove recurrence of the modeled state. Instruction diagnostics retain
|
||||
// their legacy interval-window result separately.
|
||||
bool verifyPeriodicState = false;
|
||||
// Optional diagnostic cap. The complete-state proof keeps its original budget.
|
||||
std::optional<size_t> intervalWindowEventBudget;
|
||||
bool memoryController = true;
|
||||
bool detailed = false;
|
||||
// Apply the oracle's fp32-to-int8 timing projection without changing the program.
|
||||
bool oraclePrecision = false;
|
||||
};
|
||||
|
||||
llvm::json::Object analyzePimExecution(const PimExecutionProgram &program,
|
||||
const PimInstructionTiming &target, PimPeriodicOptions options = {});
|
||||
PimPeriodicEstimate evaluatePimExecution(const PimExecutionProgram &program,
|
||||
const PimInstructionTiming &target, PimPeriodicOptions options = {});
|
||||
|
||||
uint64_t getPimLocalTransferWorkNs(const PimInstructionTiming &target,
|
||||
uint64_t bytes, bool read, bool write, bool instructionIssue = true);
|
||||
uint64_t getPimVectorWorkNs(const PimInstructionTiming &target,
|
||||
uint64_t elements, uint64_t inputBytes, uint64_t outputBytes,
|
||||
bool binary, bool instructionIssue = true);
|
||||
uint64_t getPimMatrixOutputBytes(const PimInstructionTiming &target,
|
||||
uint64_t cellBits, uint64_t weightBits, uint64_t groups, uint64_t outputBytes);
|
||||
uint64_t getPimMatrixWorkNs(const PimInstructionTiming &target,
|
||||
uint64_t inputBits, uint64_t outputBytes, bool instructionIssue = true);
|
||||
|
||||
llvm::json::Object analyzePimInstructions(const PimInstructionProgram &program,
|
||||
const PimInstructionTiming &target, int64_t cellBits,
|
||||
PimPeriodicOptions options = {});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "llvm/Support/FormatVariadic.h"
|
||||
#include "llvm/Support/JSON.h"
|
||||
#include "llvm/Support/raw_os_ostream.h"
|
||||
#include <algorithm>
|
||||
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
|
||||
#include <chrono>
|
||||
|
||||
using namespace mlir;
|
||||
@@ -57,6 +57,14 @@ struct CandidateTiming final : PassInstrumentation {
|
||||
}
|
||||
};
|
||||
|
||||
void writeSelectionReport(llvm::json::Object report) {
|
||||
auto file = openReportFileWithExtension("pipeline_selection", "json");
|
||||
if (file) {
|
||||
llvm::raw_os_ostream out(file);
|
||||
out << llvm::formatv("{0:2}\n", llvm::json::Value(std::move(report)));
|
||||
}
|
||||
}
|
||||
|
||||
struct PimPipelineSelectionPass final : PassWrapper<PimPipelineSelectionPass, OperationPass<ModuleOp>> {
|
||||
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PimPipelineSelectionPass)
|
||||
PimPipelineSelectionPass(const SchedulingTarget &target, const SpatialTargetResources &resources,
|
||||
@@ -68,156 +76,256 @@ struct PimPipelineSelectionPass final : PassWrapper<PimPipelineSelectionPass, Op
|
||||
SpatialDataflowExportStage exportStage;
|
||||
|
||||
StringRef getArgument() const override { return "pim-pipeline-selection"; }
|
||||
StringRef getDescription() const override { return "Select a throughput pipeline using static instruction queue analysis"; }
|
||||
StringRef getDescription() const override { return "Select a throughput pipeline using static Spatial queue analysis"; }
|
||||
|
||||
LogicalResult lower(ModuleOp module, llvm::json::Object *summary = nullptr) {
|
||||
PassManager pm(module.getContext());
|
||||
addPimLoweringPasses(pm, resources, EmitPimCodegen, /*reportProgress=*/false);
|
||||
if (enableTiming && summary)
|
||||
pm.addInstrumentation(std::make_unique<CandidateTiming>(*summary));
|
||||
return pm.run(module);
|
||||
}
|
||||
|
||||
LogicalResult evaluate(ModuleOp checkpoint, const MergeScheduleResult &schedule,
|
||||
OwningOpRef<ModuleOp> &candidate,
|
||||
std::string &scheduleReport, llvm::json::Object &summary) {
|
||||
LogicalResult realizeCandidate(ModuleOp checkpoint, const MergeScheduleResult &schedule,
|
||||
OwningOpRef<ModuleOp> &candidate, std::string &scheduleReport,
|
||||
llvm::json::Object &summary) {
|
||||
auto begin = Clock::now();
|
||||
IRMapping mapping;
|
||||
candidate = cast<ModuleOp>(checkpoint->clone(mapping));
|
||||
auto remapped = remapSpatialSchedule(schedule, mapping);
|
||||
if (failed(realizeSpatialSchedule(*candidate, remapped, target, stages,
|
||||
SpatialDataflowExportStage::None, nullptr, &scheduleReport))
|
||||
|| failed(lower(*candidate, &summary))) return failure();
|
||||
if (enableTiming) summary["codegen_input_ir"] = countIR(candidate->getOperation());
|
||||
summary["lowering_seconds"] = elapsed(begin);
|
||||
SpatialDataflowExportStage::None, nullptr, &scheduleReport))) return failure();
|
||||
summary["realization_seconds"] = elapsed(begin);
|
||||
return success();
|
||||
}
|
||||
|
||||
WholeScheduleCost spatialCost(ModuleOp candidate, llvm::json::Object &summary) {
|
||||
WholeScheduleCost cost;
|
||||
auto begin = Clock::now();
|
||||
llvm::json::Object estimate;
|
||||
if (target.estimateRealizedScheduleCost)
|
||||
cost = target.estimateRealizedScheduleCost(candidate, estimate);
|
||||
else cost.reason = target.wholeScheduleCostUnavailable.empty()
|
||||
? "target has no realized-Spatial cost query" : target.wholeScheduleCostUnavailable;
|
||||
cost.selectionMargin = kPipelineSelectionMargin;
|
||||
estimate["safe_for_selection"] = cost.eligible();
|
||||
estimate["diagnostic_only"] = false;
|
||||
summary["spatial_cost"] = std::move(estimate);
|
||||
summary["spatial_cost_seconds"] = elapsed(begin);
|
||||
return cost;
|
||||
}
|
||||
|
||||
LogicalResult evaluateSpatialCandidates(ModuleOp checkpoint,
|
||||
const PreparedSpatialSchedules &prepared, std::vector<WholeScheduleCost> &costs,
|
||||
llvm::json::Object &report) {
|
||||
llvm::json::Array summaries;
|
||||
double realization = 0, extraction = 0, expansion = 0, queue = 0;
|
||||
std::string errors;
|
||||
for (size_t i = 0; i < costs.size(); ++i) {
|
||||
llvm::json::Object summary{{"index", static_cast<int64_t>(i)}};
|
||||
OwningOpRef<ModuleOp> candidate;
|
||||
std::string scheduleReport;
|
||||
{
|
||||
ScopedCompilerOutputSuppression silence;
|
||||
if (failed(realizeCandidate(checkpoint, prepared.candidates[i], candidate, scheduleReport, summary)))
|
||||
costs[i].reason = "Spatial schedule realization failed verification";
|
||||
else costs[i] = spatialCost(*candidate, summary);
|
||||
}
|
||||
realization += summary.getNumber("realization_seconds").value_or(0);
|
||||
extraction += costs[i].timing.extractionSeconds;
|
||||
expansion += costs[i].timing.expansionSeconds;
|
||||
queue += costs[i].timing.evaluationSeconds;
|
||||
bool supported = costs[i].loweringSupport == StaticLoweringSupport::Supported;
|
||||
auto convergence = costs[i].convergenceProved();
|
||||
auto support = costs[i].loweringSupported();
|
||||
bool converged = convergence == true;
|
||||
bool safe = costs[i].eligible();
|
||||
summary["cost_available"] = costs[i].available();
|
||||
summary["convergence_proved"] = convergence
|
||||
? llvm::json::Value(*convergence) : llvm::json::Value(nullptr);
|
||||
summary["lowering_supported"] = support
|
||||
? llvm::json::Value(*support) : llvm::json::Value(nullptr);
|
||||
summary["eligible"] = safe;
|
||||
if (costs[i].primary) summary["interval_ns"] = *costs[i].primary;
|
||||
if (costs[i].alternative) summary["alternative_tie_interval_ns"] = *costs[i].alternative;
|
||||
if (!safe) {
|
||||
std::string property;
|
||||
if (!costs[i].available()) property += "unavailable timing; ";
|
||||
if (!supported) property += "unsupported or unknown static lowering; ";
|
||||
if (!converged) property += "unproved modeled-state convergence; ";
|
||||
if (property.empty()) property = "unavailable selection margin";
|
||||
summary["reason"] = property + ": " + costs[i].reason;
|
||||
errors += "\ncandidate " + std::to_string(i) + ": " + property + ": " + costs[i].reason;
|
||||
}
|
||||
summaries.push_back(std::move(summary));
|
||||
}
|
||||
report["candidates"] = std::move(summaries);
|
||||
report["candidate_realization_seconds"] = realization;
|
||||
report["event_extraction_seconds"] = extraction;
|
||||
report["event_expansion_seconds"] = expansion;
|
||||
report["queue_seconds"] = queue;
|
||||
if (!errors.empty()) {
|
||||
report["status"] = "incomplete-costs";
|
||||
report["reason"] = errors;
|
||||
checkpoint.emitError("Spatial pipeline cannot compare the complete candidate set:") << errors;
|
||||
return failure();
|
||||
}
|
||||
return success();
|
||||
}
|
||||
|
||||
void runOnOperation() override {
|
||||
ModuleOp module = getOperation();
|
||||
ModuleOp checkpoint = getOperation();
|
||||
auto start = Clock::now();
|
||||
std::string unsupported;
|
||||
if (!target.prepareScheduleCost)
|
||||
unsupported = target.wholeScheduleCostUnavailable.empty()
|
||||
? "target has no whole-schedule cost query" : target.wholeScheduleCostUnavailable;
|
||||
auto prepared = prepareSpatialSchedules(module, target, stages, unsupported.empty());
|
||||
auto prepared = prepareSpatialSchedules(checkpoint, target, stages, /*collectCandidates=*/true);
|
||||
if (failed(prepared)) { signalPassFailure(); return; }
|
||||
bool singleCandidate = unsupported.empty() && prepared->candidates.size() == 1;
|
||||
double generationSeconds = elapsed(start);
|
||||
std::vector<WholeScheduleCost> scores(prepared->candidates.size());
|
||||
llvm::json::Array summaries;
|
||||
OwningOpRef<ModuleOp> bestModule;
|
||||
std::string bestScheduleReport;
|
||||
size_t bestIndex = 0;
|
||||
double loweringSeconds = 0, instructionSeconds = 0, queueSeconds = 0;
|
||||
size_t lowered = 0, analyzed = 0, unavailable = 0, duplicates = 0;
|
||||
for (size_t i = 0; i < prepared->candidates.size(); ++i) {
|
||||
llvm::json::Object summary{{"index", static_cast<int64_t>(i)}};
|
||||
if (!unsupported.empty()) {
|
||||
scores[i].reason = unsupported;
|
||||
} else if (singleCandidate) {
|
||||
scores[i].reason = "one distinct generated schedule; no cost comparison required";
|
||||
} else {
|
||||
ScopedCompilerOutputSuppression silence;
|
||||
OwningOpRef<ModuleOp> candidate;
|
||||
std::string scheduleReport;
|
||||
if (failed(evaluate(module, prepared->candidates[i], candidate, scheduleReport, summary))) {
|
||||
if (!i) { signalPassFailure(); return; }
|
||||
scores[i].reason = "candidate lowering failed verification";
|
||||
} else {
|
||||
++lowered;
|
||||
loweringSeconds += *summary.getNumber("lowering_seconds");
|
||||
auto query = target.prepareScheduleCost(*candidate, summary);
|
||||
if (failed(query)) {
|
||||
if (!i) { signalPassFailure(); return; }
|
||||
scores[i].reason = "candidate cost preparation failed verification";
|
||||
++unavailable;
|
||||
summary["eligible"] = false;
|
||||
summary["reason"] = scores[i].reason;
|
||||
summaries.push_back(std::move(summary));
|
||||
continue;
|
||||
}
|
||||
instructionSeconds += summary.getNumber("instruction_generation_seconds").value_or(0);
|
||||
scores[i].identity = query->identity;
|
||||
auto duplicate = std::find_if(scores.begin(), scores.begin() + i,
|
||||
[&](const WholeScheduleCost &other) { return other.identity == scores[i].identity; });
|
||||
if (duplicate != scores.begin() + i) {
|
||||
++duplicates;
|
||||
scores[i].reason = "duplicate emitted instruction program";
|
||||
summary["duplicate_of"] = static_cast<int64_t>(duplicate - scores.begin());
|
||||
} else {
|
||||
auto begin = Clock::now();
|
||||
scores[i] = query->evaluate(summary);
|
||||
scores[i].identity = query->identity;
|
||||
double seconds = elapsed(begin);
|
||||
queueSeconds += seconds;
|
||||
++analyzed;
|
||||
summary["queue_seconds"] = seconds;
|
||||
}
|
||||
std::string partialReason;
|
||||
size_t partialSelection = selectPipelineCandidate(
|
||||
llvm::ArrayRef<WholeScheduleCost>(scores.data(), i + 1), partialReason);
|
||||
if (!bestModule || partialSelection == i) {
|
||||
bestIndex = i;
|
||||
bestModule = std::move(candidate);
|
||||
bestScheduleReport = std::move(scheduleReport);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!singleCandidate && !scores[i].eligible()
|
||||
&& scores[i].reason != "duplicate emitted instruction program") ++unavailable;
|
||||
summary["eligible"] = scores[i].eligible();
|
||||
summary["identity"] = scores[i].identity;
|
||||
if (scores[i].primary) summary["interval_ns"] = *scores[i].primary;
|
||||
if (scores[i].alternative) summary["alternative_tie_interval_ns"] = *scores[i].alternative;
|
||||
if (scores[i].relativeUncertainty)
|
||||
summary["relative_prediction_uncertainty"] = *scores[i].relativeUncertainty;
|
||||
if (!scores[i].reason.empty()) summary["reason"] = scores[i].reason;
|
||||
summaries.push_back(std::move(summary));
|
||||
if (prepared->candidates.empty()) {
|
||||
checkpoint.emitError("Spatial pipeline requires at least one candidate schedule");
|
||||
signalPassFailure(); return;
|
||||
}
|
||||
bool single = prepared->candidates.size() == 1;
|
||||
llvm::json::Object report{{"strategy", "spatial-queue-aware"}, {"static_analysis", true},
|
||||
{"candidate_generation_seconds", elapsed(start)},
|
||||
{"generated_candidates", static_cast<int64_t>(prepared->candidates.size())},
|
||||
{"single_candidate_analysis_skipped", single}, {"selection_margin", kPipelineSelectionMargin},
|
||||
{"prediction_error_bound_available", false}, {"temporary_disk_bytes", 0}};
|
||||
std::vector<WholeScheduleCost> costs(prepared->candidates.size());
|
||||
if (!single && failed(evaluateSpatialCandidates(checkpoint, *prepared, costs, report))) {
|
||||
writeSelectionReport(std::move(report)); signalPassFailure(); return;
|
||||
}
|
||||
if (single) report["candidates"] = llvm::json::Array{llvm::json::Object{{"index", 0}}};
|
||||
OwningOpRef<ModuleOp> winner;
|
||||
std::string scheduleReport;
|
||||
llvm::json::Array attempts;
|
||||
double realizationSeconds = 0, backendSeconds = 0;
|
||||
size_t lowered = 0;
|
||||
auto attempt = [&](size_t index) {
|
||||
llvm::json::Object summary{{"candidate", static_cast<int64_t>(index)}};
|
||||
std::string capturedReport;
|
||||
OwningOpRef<ModuleOp> candidate;
|
||||
LogicalResult realized = failure();
|
||||
{
|
||||
ScopedCompilerOutputSuppression silence;
|
||||
realized = realizeCandidate(checkpoint, prepared->candidates[index], candidate, capturedReport, summary);
|
||||
}
|
||||
PimCandidateResult result = PimCandidateResult::Failure;
|
||||
if (failed(realized)) {
|
||||
summary["reason"] = "selected Spatial schedule failed deterministic realization";
|
||||
summary["status"] = "realization-failed";
|
||||
} else {
|
||||
++lowered;
|
||||
result = lowerPimPipelineCandidate(*candidate, resources, summary);
|
||||
}
|
||||
realizationSeconds += summary.getNumber("realization_seconds").value_or(0);
|
||||
backendSeconds += summary.getNumber("backend_seconds").value_or(0);
|
||||
if (result == PimCandidateResult::Success) {
|
||||
summary["status"] = "lowered";
|
||||
winner = std::move(candidate);
|
||||
scheduleReport = std::move(capturedReport);
|
||||
} else if (result == PimCandidateResult::ResourceLimit)
|
||||
llvm::errs() << "Spatial pipeline candidate " << index
|
||||
<< " rejected by a backend resource limit; rejection recorded in pipeline_selection.json\n";
|
||||
attempts.push_back(std::move(summary));
|
||||
return result;
|
||||
};
|
||||
std::string reason;
|
||||
size_t selected = selectPipelineCandidate(scores, reason);
|
||||
if (singleCandidate) reason = scores.front().reason;
|
||||
else if (!unsupported.empty()) reason += ": " + unsupported;
|
||||
else if (!scores.front().eligible() && !scores.front().reason.empty())
|
||||
reason += ": " + scores.front().reason;
|
||||
auto finalizeStart = Clock::now();
|
||||
std::optional<size_t> selected;
|
||||
if (single) {
|
||||
if (attempt(0) == PimCandidateResult::Success) selected = 0;
|
||||
reason = selected ? "one distinct schedule; no cost comparison required"
|
||||
: "the single schedule failed backend lowering; no alternatives are available";
|
||||
} else selected = lowerSelectedPimCandidate(costs, attempt, reason);
|
||||
report["backend_attempts"] = std::move(attempts);
|
||||
report["lowered_candidates"] = static_cast<int64_t>(lowered);
|
||||
report["costed_candidates"] = single ? 0 : static_cast<int64_t>(costs.size());
|
||||
size_t attemptCount = report.getArray("backend_attempts")->size();
|
||||
report["backend_retries"] = static_cast<int64_t>(attemptCount ? attemptCount - 1 : 0);
|
||||
report["winner_realization_seconds"] = realizationSeconds;
|
||||
report["backend_lowering_seconds"] = backendSeconds;
|
||||
if (!selected) {
|
||||
report["status"] = "backend-failure";
|
||||
report["reason"] = reason;
|
||||
std::string failures;
|
||||
for (auto &entry : *report.getArray("backend_attempts")) {
|
||||
auto *failed = entry.getAsObject();
|
||||
failures += "\ncandidate " + std::to_string(*failed->getInteger("candidate"))
|
||||
+ ": " + failed->getString("reason").value_or("backend failure").str();
|
||||
}
|
||||
checkpoint.emitError("Spatial pipeline backend selection failed; ") << reason << failures;
|
||||
writeSelectionReport(std::move(report)); signalPassFailure(); return;
|
||||
}
|
||||
auto finalize = Clock::now();
|
||||
bool spatialArtifacts = exportStage != SpatialDataflowExportStage::None
|
||||
|| pimPeriodicDiagnostics || isPimIrDumpEnabled();
|
||||
// Explicit Spatial diagnostics require the pre-lowering representation.
|
||||
if (!bestModule || spatialArtifacts) {
|
||||
if (failed(realizeSpatialSchedule(module, prepared->candidates[selected], target,
|
||||
stages, exportStage, pimPeriodicDiagnostics ? &prepared->graph : nullptr))) { signalPassFailure(); return; }
|
||||
|| pimPeriodicDiagnostics || isPimIrDumpEnabled();
|
||||
if (spatialArtifacts) {
|
||||
if (failed(realizeSpatialSchedule(checkpoint, prepared->candidates[*selected], target,
|
||||
stages, exportStage, pimPeriodicDiagnostics ? &prepared->graph : nullptr))) {
|
||||
signalPassFailure(); return;
|
||||
}
|
||||
} else {
|
||||
auto file = openDialectDumpFileWithExtension("spatial3_scheduled_no_comm", "/reports", "txt");
|
||||
if (file) file << bestScheduleReport;
|
||||
if (file) file << scheduleReport;
|
||||
}
|
||||
if (bestModule) {
|
||||
assert(selected == bestIndex && "retained module must be the selected candidate");
|
||||
module->setAttrs((*bestModule)->getAttrs());
|
||||
module.getBodyRegion().takeBody(bestModule->getBodyRegion());
|
||||
} else if (failed(lower(module))) { signalPassFailure(); return; }
|
||||
llvm::errs() << "Queue-aware pipeline: candidate " << selected << " of " << scores.size()
|
||||
checkpoint->setAttrs((*winner)->getAttrs());
|
||||
checkpoint.getBodyRegion().takeBody(winner->getBodyRegion());
|
||||
report["selection"] = static_cast<int64_t>(*selected);
|
||||
report["status"] = "selected";
|
||||
report["reason"] = reason;
|
||||
report["finalization_seconds"] = elapsed(finalize);
|
||||
report["total_seconds"] = elapsed(start);
|
||||
llvm::errs() << "Spatial queue-aware pipeline: candidate " << *selected << " of " << costs.size()
|
||||
<< "; " << reason << "\n";
|
||||
llvm::json::Object report{{"selection", static_cast<int64_t>(selected)}, {"reason", reason},
|
||||
{"static_analysis", true}, {"strategy", "queue-aware"}, {"candidates", std::move(summaries)},
|
||||
{"single_candidate_analysis_skipped", singleCandidate},
|
||||
{"candidate_generation_seconds", generationSeconds}, {"candidate_lowering_seconds", loweringSeconds},
|
||||
{"instruction_generation_seconds", instructionSeconds},
|
||||
{"queue_seconds", queueSeconds}, {"finalization_seconds", elapsed(finalizeStart)}, {"total_seconds", elapsed(start)},
|
||||
{"generated_candidates", static_cast<int64_t>(scores.size())},
|
||||
{"lowered_candidates", static_cast<int64_t>(lowered)},
|
||||
{"reused_lowered_candidate", static_cast<bool>(bestModule)},
|
||||
{"reconstructed_spatial_artifacts", spatialArtifacts && static_cast<bool>(bestModule)},
|
||||
{"analyzed_candidates", static_cast<int64_t>(analyzed)},
|
||||
{"unavailable_estimates", static_cast<int64_t>(unavailable)},
|
||||
{"duplicate_programs", static_cast<int64_t>(duplicates)}, {"temporary_disk_bytes", 0}};
|
||||
auto file = openReportFileWithExtension("pipeline_selection", "json");
|
||||
if (file) { llvm::raw_os_ostream out(file); out << llvm::formatv("{0:2}\n", llvm::json::Value(std::move(report))); }
|
||||
writeSelectionReport(std::move(report));
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
PimCandidateResult onnx_mlir::lowerPimPipelineCandidate(ModuleOp module,
|
||||
const SpatialTargetResources &resources, llvm::json::Object &summary) {
|
||||
bool classified = true, anyError = false;
|
||||
std::string diagnostic;
|
||||
size_t count = 0;
|
||||
ScopedDiagnosticHandler handler(module.getContext(), [&](Diagnostic &message) {
|
||||
if (message.getSeverity() == DiagnosticSeverity::Error) {
|
||||
anyError = true;
|
||||
classified &= pim::isCandidateResourceError(message);
|
||||
if (++count <= 8) diagnostic += message.str().substr(0, 2048) + "\n";
|
||||
}
|
||||
return success();
|
||||
});
|
||||
auto start = Clock::now();
|
||||
PassManager pm(module.getContext());
|
||||
addPimLoweringPasses(pm, resources, EmitPimCodegen, /*reportProgress=*/false);
|
||||
if (enableTiming) pm.addInstrumentation(std::make_unique<CandidateTiming>(summary));
|
||||
LogicalResult result = pm.run(module);
|
||||
summary["backend_seconds"] = elapsed(start);
|
||||
if (succeeded(result)) return PimCandidateResult::Success;
|
||||
summary["reason"] = diagnostic.empty() ? "backend failed without a classified diagnostic" : diagnostic;
|
||||
bool resource = anyError && classified;
|
||||
summary["status"] = resource ? "resource-limit" : "systemic-or-unclassified-error";
|
||||
return resource ? PimCandidateResult::ResourceLimit : PimCandidateResult::Failure;
|
||||
}
|
||||
|
||||
std::optional<size_t> onnx_mlir::lowerSelectedPimCandidate(llvm::ArrayRef<WholeScheduleCost> costs,
|
||||
const std::function<PimCandidateResult(size_t)> &lower, std::string &reason) {
|
||||
if (costs.empty()) {
|
||||
reason = "Spatial pipeline requires at least one candidate schedule";
|
||||
return std::nullopt;
|
||||
}
|
||||
for (const auto &cost : costs)
|
||||
if (!cost.eligible()) {
|
||||
reason = "cannot compare the complete candidate set: unavailable cost or unproved eligibility";
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<WholeScheduleCost> remaining(costs.begin(), costs.end());
|
||||
std::vector<size_t> indices;
|
||||
for (size_t i = 0; i < costs.size(); ++i) indices.push_back(i);
|
||||
while (!indices.empty()) {
|
||||
size_t selected = selectPipelineCandidate(remaining, reason);
|
||||
auto result = lower(indices[selected]);
|
||||
if (result == PimCandidateResult::Success) return indices[selected];
|
||||
if (result != PimCandidateResult::ResourceLimit) {
|
||||
reason = "systemic or unclassified backend failure; recovery is not justified";
|
||||
return std::nullopt;
|
||||
}
|
||||
remaining.erase(remaining.begin() + selected);
|
||||
indices.erase(indices.begin() + selected);
|
||||
}
|
||||
reason = "all candidate schedules failed backend resource constraints";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::unique_ptr<Pass> onnx_mlir::createPimPipelineSelectionPass(
|
||||
const SchedulingTarget &target, const SpatialTargetResources &resources,
|
||||
size_t stages, SpatialDataflowExportStage exportStage) {
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
#pragma once
|
||||
#include "mlir/Pass/Pass.h"
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace mlir { class ModuleOp; }
|
||||
namespace llvm::json { class Object; }
|
||||
|
||||
namespace onnx_mlir {
|
||||
namespace spatial { struct SchedulingTarget; struct SpatialTargetResources; enum class SpatialDataflowExportStage; }
|
||||
namespace spatial { struct SchedulingTarget; struct SpatialTargetResources; struct WholeScheduleCost; enum class SpatialDataflowExportStage; }
|
||||
enum class PimCandidateResult { Success, ResourceLimit, Failure };
|
||||
PimCandidateResult lowerPimPipelineCandidate(mlir::ModuleOp module,
|
||||
const spatial::SpatialTargetResources &resources, llvm::json::Object &summary);
|
||||
// Callback failures distinguish owner-classified resource limits from other errors.
|
||||
std::optional<size_t> lowerSelectedPimCandidate(llvm::ArrayRef<spatial::WholeScheduleCost> costs,
|
||||
const std::function<PimCandidateResult(size_t)> &lower, std::string &reason);
|
||||
std::unique_ptr<mlir::Pass> createPimPipelineSelectionPass(
|
||||
const spatial::SchedulingTarget &target, const spatial::SpatialTargetResources &resources,
|
||||
size_t stages, spatial::SpatialDataflowExportStage exportStage);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "PimPeriodicAnalysis.hpp"
|
||||
|
||||
using namespace onnx_mlir;
|
||||
|
||||
namespace {
|
||||
const char *windowStatus(const PimQueuePolicyResult &policy) {
|
||||
if (policy.legacy.intervalNs) return "estimated";
|
||||
return policy.legacy.termination == PimQueueTermination::NotRun && policy.recurrence.proved()
|
||||
? "not-required" : "unavailable";
|
||||
}
|
||||
|
||||
const char *terminationName(PimQueueTermination termination) {
|
||||
switch (termination) {
|
||||
case PimQueueTermination::NotRun: return "not-run";
|
||||
case PimQueueTermination::IntervalConverged: return "interval-converged";
|
||||
case PimQueueTermination::RecurrenceProved: return "recurrence-proved";
|
||||
case PimQueueTermination::EventBudget: return "event-budget";
|
||||
case PimQueueTermination::GenerationLimit: return "generation-limit";
|
||||
case PimQueueTermination::HistoryLimit: return "history-limit";
|
||||
case PimQueueTermination::NonMonotonic: return "non-monotonic";
|
||||
case PimQueueTermination::NoReadyEvents: return "no-ready-events";
|
||||
}
|
||||
llvm_unreachable("invalid queue termination");
|
||||
}
|
||||
|
||||
void serializeProgress(llvm::json::Object &report, const PimQueueProgress &progress,
|
||||
bool estimated, bool details, bool intervals) {
|
||||
if (progress.processedEvents) report["processed_events"] = progress.processedEvents;
|
||||
if (estimated) {
|
||||
report["completed_generations"] = progress.completedGenerations;
|
||||
report["maximum_queue_wait_ns_including_bootstrap"] = progress.maximumQueueWaitNs;
|
||||
if (progress.maximumQueueRequest) {
|
||||
report["maximum_queue_core"] = progress.maximumQueueRequest->core;
|
||||
report["maximum_queue_pc"] = progress.maximumQueueRequest->pc;
|
||||
}
|
||||
if (details) {
|
||||
llvm::json::Array cores;
|
||||
for (auto [core, wait] : progress.perCoreQueueWaitNs)
|
||||
cores.push_back(llvm::json::Object{{"core", core}, {"queue_wait_ns", wait}});
|
||||
report["per_core"] = std::move(cores);
|
||||
}
|
||||
}
|
||||
if (progress.generationLimit) {
|
||||
report["generation_limit_core"] = progress.generationLimit->core;
|
||||
report["generation_limit_pc"] = progress.generationLimit->pc;
|
||||
report["generation_limit_kind"] = progress.generationLimit->kind;
|
||||
}
|
||||
if (intervals) {
|
||||
llvm::json::Array values;
|
||||
for (uint64_t value : progress.completedIntervalsNs) values.push_back(value);
|
||||
report["completed_interval_ns"] = std::move(values);
|
||||
}
|
||||
}
|
||||
|
||||
llvm::json::Object serializeWindow(const PimQueuePolicyResult &policy, bool details) {
|
||||
const auto &window = policy.legacy;
|
||||
llvm::json::Object result{{"status", windowStatus(policy)}};
|
||||
if (window.intervalNs) {
|
||||
result["interval_ns"] = *window.intervalNs;
|
||||
result["tie_policy"] = policy.reverseTies ? "reverse event-index order for simultaneous readiness"
|
||||
: "event-index order for simultaneous readiness";
|
||||
}
|
||||
if (!window.reason.empty()) result["reason"] = window.reason;
|
||||
serializeProgress(result, window.progress, window.intervalNs.has_value(), details,
|
||||
window.termination == PimQueueTermination::EventBudget
|
||||
|| window.termination == PimQueueTermination::NoReadyEvents);
|
||||
return result;
|
||||
}
|
||||
|
||||
llvm::json::Object serializeRecurrence(const PimQueuePolicyResult &policy) {
|
||||
const auto &evidence = policy.recurrence;
|
||||
llvm::json::Object result{{"status", evidence.proved() ? "proved" : "unproved"},
|
||||
{"termination", terminationName(policy.termination)}};
|
||||
if (policy.checkpoints) {
|
||||
result["checkpoints"] = policy.checkpoints;
|
||||
result["history_bytes"] = policy.historyBytes;
|
||||
}
|
||||
if (evidence.proved()) {
|
||||
result["period_generations"] = evidence.periodGenerations;
|
||||
result["interval_ns"] = *evidence.intervalNs;
|
||||
result["state_words"] = policy.stateWords;
|
||||
result["complete_scheduler_state"] = true;
|
||||
result["proof_processed_events"] = policy.proofProcessedEvents;
|
||||
}
|
||||
if (!evidence.reason.empty()) result["reason"] = evidence.reason;
|
||||
return result;
|
||||
}
|
||||
|
||||
void addAlternative(llvm::json::Object &report, const PimQueuePolicyResult &policy, bool legacy) {
|
||||
auto interval = legacy ? policy.legacy.intervalNs : policy.intervalNs();
|
||||
report["alternative_tie_status"] = legacy ? windowStatus(policy) : interval ? "estimated" : "unavailable";
|
||||
if (interval) report["alternative_tie_interval_ns"] = *interval;
|
||||
if ((legacy || !policy.recurrence.proved()) && !policy.legacy.reason.empty())
|
||||
report["alternative_tie_reason"] = policy.legacy.reason;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
llvm::json::Object onnx_mlir::serializePimQueueEvaluation(const PimQueueEvaluation &evaluation) {
|
||||
const auto &primary = evaluation.policies[0];
|
||||
const auto &alternative = evaluation.policies[1];
|
||||
llvm::json::Object report;
|
||||
if (evaluation.recurrenceRequired && primary.recurrence.proved()) {
|
||||
report = llvm::json::Object{{"status", "estimated"}, {"interval_ns", *primary.intervalNs()},
|
||||
{"tie_policy", primary.reverseTies ? "reverse event-index order for simultaneous readiness"
|
||||
: "event-index order for simultaneous readiness"}};
|
||||
serializeProgress(report, primary.progress, true, evaluation.details, false);
|
||||
} else report = serializeWindow(primary, evaluation.details);
|
||||
if (evaluation.recurrenceRequired) {
|
||||
auto legacy = serializeWindow(primary, evaluation.details);
|
||||
legacy["termination"] = terminationName(primary.legacy.termination);
|
||||
legacy["alternative_termination"] = terminationName(alternative.legacy.termination);
|
||||
addAlternative(legacy, alternative, true);
|
||||
report["interval_source"] = primary.recurrence.proved() ? "complete-state recurrence" : "legacy interval window";
|
||||
report["alternative_interval_source"] = alternative.recurrence.proved() ? "complete-state recurrence" : "legacy interval window";
|
||||
report["periodic_state"] = serializeRecurrence(primary);
|
||||
report["alternative_periodic_state"] = serializeRecurrence(alternative);
|
||||
report["legacy_interval_window"] = std::move(legacy);
|
||||
}
|
||||
addAlternative(report, alternative, false);
|
||||
if (auto interval = primary.intervalNs())
|
||||
report["memory_service_fraction"] = evaluation.memoryServiceNs / *interval;
|
||||
if (evaluation.measureTime) report["analysis_ms"] = evaluation.evaluationSeconds * 1000;
|
||||
report["arbitration"] = "FIFO by predicted controller arrival";
|
||||
return report;
|
||||
}
|
||||
@@ -357,6 +357,7 @@ static mlir::FailureOr<PeriodicDependencyEstimate> analyzePlan(PeriodicPlan &pla
|
||||
return analyzePeriodicDependencies(plan.events.size(), plan.edges, plan.error);
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
llvm::json::Object getPeriodicScheduleReport(const ComputeGraph &graph,
|
||||
|
||||
@@ -0,0 +1,623 @@
|
||||
#include "PimSpatialCost.hpp"
|
||||
#include "PimInstructionTiming.hpp"
|
||||
#include "PimTimingArithmetic.hpp"
|
||||
#include "PimBinaryFormat.hpp"
|
||||
#include "PimCompilerUtils.hpp"
|
||||
#include "PimCompilerOptions.hpp"
|
||||
#include "PimScheduleDiagnostic.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/CopyAccessPlan.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ShapingUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/ComputeLikeRegionUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.hpp"
|
||||
#include "mlir/Dialect/Linalg/IR/Linalg.h"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
#include "mlir/Dialect/SCF/IR/SCF.h"
|
||||
#include "mlir/IR/Matchers.h"
|
||||
#include "llvm/ADT/SmallPtrSet.h"
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "llvm/Support/MathExtras.h"
|
||||
#include <chrono>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <limits>
|
||||
|
||||
using namespace mlir;
|
||||
using namespace onnx_mlir;
|
||||
using namespace onnx_mlir::spatial;
|
||||
using pim_timing::add;
|
||||
using pim_timing::mul;
|
||||
using pim_timing::ceil;
|
||||
namespace {
|
||||
using Event = PimExecutionEvent;
|
||||
using Sequence = std::vector<Event>;
|
||||
enum class Residence { Local, Host, Constant, Zero, CapturedWeight };
|
||||
|
||||
PimPeriodicEstimate unavailableCost(llvm::StringRef reason) {
|
||||
PimPeriodicEstimate result;
|
||||
result.cost.reason = reason.str();
|
||||
result.report = llvm::json::Object{{"status", "unavailable"}, {"cost_available", false},
|
||||
{"convergence_proved", nullptr}, {"lowering_supported", nullptr},
|
||||
{"safe_for_selection", false}, {"reason", reason.str()}};
|
||||
return result;
|
||||
}
|
||||
|
||||
class Extraction {
|
||||
public:
|
||||
const PimInstructionTiming ⌖
|
||||
bool diagnostics;
|
||||
PimExecutionProgram program;
|
||||
std::string error, loweringReason;
|
||||
StaticLoweringSupport loweringSupport = StaticLoweringSupport::Supported;
|
||||
std::map<std::string, int64_t> gaps;
|
||||
int64_t visited = 0, constantLoads = 0;
|
||||
explicit Extraction(const PimInstructionTiming &target, bool diagnostics)
|
||||
: target(target), diagnostics(diagnostics), program(target.processorCount) {}
|
||||
bool build(ModuleOp module);
|
||||
private:
|
||||
llvm::SmallDenseSet<int64_t, 16> assignedCores;
|
||||
DenseMap<Value, Residence> residence;
|
||||
DenseMap<Value, CopyAccessPlan> publicationAccess;
|
||||
DenseMap<Value, SmallVector<unsigned, 2>> directOutputs;
|
||||
llvm::SmallDenseSet<Value, 4> compactStorage;
|
||||
llvm::SmallPtrSet<Operation *, 32> indexed;
|
||||
StaticIndexEnvironment environment;
|
||||
Residence storage(Value value);
|
||||
uint64_t elements(Type type) {
|
||||
auto shape = dyn_cast<ShapedType>(type);
|
||||
if (!shape || !shape.hasStaticShape()) {
|
||||
error = "abstract work requires a static shaped payload";
|
||||
return 0;
|
||||
}
|
||||
if (!shape.getElementType().isIntOrFloat() || shape.getElementTypeBitWidth() != 32) {
|
||||
error = "abstract precision projection requires 32-bit elements";
|
||||
return 0;
|
||||
}
|
||||
uint64_t result = 1;
|
||||
for (int64_t dimension : shape.getShape()) result = mul(result, dimension);
|
||||
return result;
|
||||
}
|
||||
uint64_t bytes(Type type) {
|
||||
uint64_t count = elements(type);
|
||||
auto shaped = dyn_cast<ShapedType>(type);
|
||||
if (!shaped || !shaped.getElementType().isIntOrFloat()) {
|
||||
error = "abstract transfer requires a scalar integer or floating element";
|
||||
return 0;
|
||||
}
|
||||
uint64_t nativeBytes = ceil(mul(count, shaped.getElementTypeBitWidth()), 8);
|
||||
if (nativeBytes > std::numeric_limits<int32_t>::max()) {
|
||||
loweringSupport = StaticLoweringSupport::Unknown;
|
||||
loweringReason = "native transfer size requires backend normalization and size verification";
|
||||
}
|
||||
return ceil(mul(count, target.computeBitwidth), 8);
|
||||
}
|
||||
void work(Sequence &sequence, uint64_t ns, Event::WorkClass resource = Event::LocalCopy) {
|
||||
if (!ns) return;
|
||||
if (!sequence.empty() && sequence.back().kind == Event::Work
|
||||
&& sequence.back().workClass == resource)
|
||||
sequence.back().amount = add(sequence.back().amount, ns);
|
||||
else {
|
||||
sequence.push_back({Event::Work, ns});
|
||||
sequence.back().workClass = resource;
|
||||
}
|
||||
}
|
||||
uint64_t localWork(uint64_t bytes, bool read, bool write) const {
|
||||
return getPimLocalTransferWorkNs(target, bytes, read, write);
|
||||
}
|
||||
void materialize(Value value, Sequence &sequence);
|
||||
bool accessSlice(tensor::ExtractSliceOp slice, Sequence &sequence, bool host);
|
||||
bool block(Block &body, Sequence &sequence);
|
||||
bool operation(Operation &op, Sequence &sequence);
|
||||
bool loop(scf::ForOp op, Sequence &sequence);
|
||||
bool core(Operation *op, int64_t physicalCore, unsigned lane);
|
||||
};
|
||||
|
||||
Residence Extraction::storage(Value value) {
|
||||
if (auto it = residence.find(value); it != residence.end()) return it->second;
|
||||
Attribute constant;
|
||||
if (matchPattern(value, m_Constant(&constant)))
|
||||
return isZeroBitPatternSplat(constant) ? Residence::Zero : Residence::Constant;
|
||||
if (auto slice = value.getDefiningOp<tensor::ExtractSliceOp>()) return storage(slice.getSource());
|
||||
if (auto collapse = value.getDefiningOp<tensor::CollapseShapeOp>()) return storage(collapse.getSrc());
|
||||
if (auto expand = value.getDefiningOp<tensor::ExpandShapeOp>()) return storage(expand.getSrc());
|
||||
if (auto cast = value.getDefiningOp<tensor::CastOp>()) return storage(cast.getSource());
|
||||
if (auto branch = value.getDefiningOp<scf::IfOp>()) {
|
||||
auto condition = evaluateDeferredIndex(branch.getCondition(), environment);
|
||||
if (succeeded(condition)) {
|
||||
Region ®ion = *condition ? branch.getThenRegion() : branch.getElseRegion();
|
||||
return storage(region.front().getTerminator()->getOperand(cast<OpResult>(value).getResultNumber()));
|
||||
}
|
||||
}
|
||||
if (auto arg = dyn_cast<BlockArgument>(value)) {
|
||||
if (auto loop = dyn_cast_or_null<scf::ForOp>(arg.getOwner()->getParentOp()))
|
||||
if (arg.getArgNumber()) return Residence::Local;
|
||||
return Residence::Host;
|
||||
}
|
||||
if (isa_and_nonnull<SpatVMMOp, SpatVAddOp, SpatVSubOp, SpatVMulOp,
|
||||
SpatVMaxOp, SpatVVDMulOp, SpatVAvgOp, SpatReluOp, SpatSigmoidOp,
|
||||
SpatChannelReceiveOp, SpatHostWaitLoadOp, SpatScheduledCompute,
|
||||
SpatScheduledComputeBatch, SpatConcatOp, tensor::EmptyOp,
|
||||
tensor::InsertSliceOp, tensor::PadOp, linalg::TransposeOp, scf::ForOp>(value.getDefiningOp()))
|
||||
return Residence::Local;
|
||||
error = "abstract storage residence is unavailable for "
|
||||
+ value.getDefiningOp()->getName().getStringRef().str();
|
||||
return Residence::Local; // The caller discards the estimate when error is set.
|
||||
}
|
||||
|
||||
void Extraction::materialize(Value value, Sequence &sequence) {
|
||||
auto place = storage(value);
|
||||
if (place == Residence::Host || place == Residence::Constant || place == Residence::CapturedWeight) {
|
||||
sequence.push_back({Event::Load, bytes(value.getType())});
|
||||
// Captured weights remain block arguments; only direct global zero loads fold to fills.
|
||||
if (place == Residence::Constant || place == Residence::CapturedWeight) {
|
||||
++constantLoads;
|
||||
sequence.back().constant = true;
|
||||
}
|
||||
} else if (place == Residence::Zero) {
|
||||
work(sequence, localWork(bytes(value.getType()), false, true), Event::Fill);
|
||||
}
|
||||
}
|
||||
|
||||
bool Extraction::accessSlice(tensor::ExtractSliceOp slice, Sequence &sequence, bool host) {
|
||||
auto source = computeRowMajorStridesChecked(slice.getSourceType().getShape());
|
||||
auto destination = computeRowMajorStridesChecked(slice.getStaticSizes());
|
||||
if (failed(source) || failed(destination)) { error = "abstract slice geometry unavailable"; return false; }
|
||||
SmallVector<int64_t> strides;
|
||||
for (auto [stride, scale] : llvm::zip(*source, slice.getStaticStrides())) {
|
||||
int64_t result;
|
||||
if (scale <= 0 || llvm::MulOverflow(stride, scale, result)) {
|
||||
error = "abstract slice stride unavailable"; return false;
|
||||
}
|
||||
strides.push_back(result);
|
||||
}
|
||||
auto copy = CopyAccessPlan::build(slice.getStaticSizes(), strides, *destination);
|
||||
if (failed(copy)) { error = "abstract slice access plan unavailable"; return false; }
|
||||
uint64_t size = ceil(mul(copy->getRunElements(), target.computeBitwidth), 8);
|
||||
if (host) {
|
||||
sequence.push_back({Event::Repeat, static_cast<uint64_t>(copy->getRunCount()),
|
||||
0, 0, 0, {{Event::Load, size}}});
|
||||
residence[slice.getResult()] = Residence::Local;
|
||||
compactStorage.insert(slice.getResult());
|
||||
} else if (compactStorage.contains(slice.getSource())
|
||||
&& slice.getSourceType().getRank() == slice.getType().getRank()
|
||||
&& llvm::all_of(slice.getResult().getUsers(), [](Operation *user) {
|
||||
return isa<SpatHostStoreSyncOp>(user);
|
||||
})) {
|
||||
// Publication-only slices retain the compact source's subview geometry.
|
||||
publicationAccess.insert_or_assign(slice.getResult(), *copy);
|
||||
} else if (copy->getRunCount() > 1) {
|
||||
if (diagnostics) ++gaps["local slice materialization and forwarding"];
|
||||
work(sequence, mul(copy->getRunCount(), localWork(size, true, true)));
|
||||
}
|
||||
if (compactStorage.contains(slice.getSource()) && copy->getRunCount() == 1)
|
||||
compactStorage.insert(slice.getResult());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Extraction::loop(scf::ForOp op, Sequence &sequence) {
|
||||
auto lower = evaluateDeferredIndex(op.getLowerBound(), environment);
|
||||
auto upper = evaluateDeferredIndex(op.getUpperBound(), environment);
|
||||
auto step = evaluateDeferredIndex(op.getStep(), environment);
|
||||
if (failed(lower) || failed(upper) || failed(step) || *step <= 0) {
|
||||
error = "abstract loop has unavailable static bounds"; return false;
|
||||
}
|
||||
for (Value initial : op.getInitArgs()) materialize(initial, sequence);
|
||||
int64_t distance = 0;
|
||||
if (*upper > *lower && llvm::SubOverflow(*upper, *lower, distance)) {
|
||||
error = "unrepresentable abstract loop trip count"; return false;
|
||||
}
|
||||
uint64_t count = ceil(distance, *step);
|
||||
bool indexSensitive = indexed.contains(op);
|
||||
if (indexSensitive && count > 100000) {
|
||||
error = "abstract indexed loop exceeds analysis budget"; return false;
|
||||
}
|
||||
if (!count) return true;
|
||||
if (indexSensitive) {
|
||||
for (uint64_t i = 0; i < count; ++i) {
|
||||
int64_t induction;
|
||||
if (llvm::AddOverflow(*lower, static_cast<int64_t>(mul(i, *step)), induction)) {
|
||||
error = "unrepresentable abstract induction value"; return false;
|
||||
}
|
||||
environment.bindings[op.getInductionVar()] = induction;
|
||||
if (!block(*op.getBody(), sequence)) return false;
|
||||
}
|
||||
} else {
|
||||
environment.bindings[op.getInductionVar()] = *lower;
|
||||
Event repeat{Event::Repeat, count};
|
||||
if (!block(*op.getBody(), repeat.body)) return false;
|
||||
sequence.push_back(std::move(repeat));
|
||||
}
|
||||
environment.bindings.erase(op.getInductionVar());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Extraction::operation(Operation &op, Sequence &sequence) {
|
||||
if (auto loopOp = dyn_cast<scf::ForOp>(op)) return loop(loopOp, sequence);
|
||||
if (auto branch = dyn_cast<scf::IfOp>(op)) {
|
||||
auto condition = evaluateDeferredIndex(branch.getCondition(), environment);
|
||||
if (failed(condition)) { error = "abstract branch condition is unavailable"; return false; }
|
||||
Region ®ion = *condition ? branch.getThenRegion() : branch.getElseRegion();
|
||||
return region.empty() || block(region.front(), sequence);
|
||||
}
|
||||
auto index = [&](Value value) -> int64_t {
|
||||
auto result = evaluateDeferredIndex(value, environment);
|
||||
if (failed(result)) { error = "abstract communication index is unavailable"; return -1; }
|
||||
return *result;
|
||||
};
|
||||
if (auto sync = dyn_cast<SpatSyncOp>(op)) {
|
||||
sequence.push_back({Event::Signal, 0, index(sync.getTargetCoreId()), index(sync.getEventRegister())});
|
||||
} else if (auto wait = dyn_cast<SpatWaitOp>(op)) {
|
||||
int64_t count = index(wait.getWaitValue());
|
||||
if (count) sequence.push_back({Event::Wait, 0, 0, index(wait.getEventRegister()), count});
|
||||
} else if (auto send = dyn_cast<SpatChannelSendOp>(op)) {
|
||||
materialize(send.getInput(), sequence);
|
||||
sequence.push_back({Event::Send, bytes(send.getInput().getType()), index(send.getTargetCoreId())});
|
||||
} else if (auto recv = dyn_cast<SpatChannelReceiveOp>(op)) {
|
||||
sequence.push_back({Event::Receive, bytes(recv.getOutput().getType()), index(recv.getSourceCoreId())});
|
||||
} else if (auto store = dyn_cast<SpatHostStoreSyncOp>(op)) {
|
||||
materialize(store.getInput(), sequence);
|
||||
if (auto plan = publicationAccess.find(store.getInput()); plan != publicationAccess.end())
|
||||
sequence.push_back({Event::Repeat, static_cast<uint64_t>(plan->second.getRunCount()),
|
||||
0, 0, 0, {{Event::Store, ceil(mul(plan->second.getRunElements(), target.computeBitwidth), 8)}}});
|
||||
else sequence.push_back({Event::Store, bytes(store.getInput().getType())});
|
||||
sequence.push_back({Event::Signal, 0, index(store.getTargetCoreId()), index(store.getEventRegister())});
|
||||
} else if (auto load = dyn_cast<SpatHostWaitLoadOp>(op)) {
|
||||
int64_t count = index(load.getWaitValue());
|
||||
if (count) sequence.push_back({Event::Wait, 0, 0, index(load.getEventRegister()), count});
|
||||
sequence.push_back({Event::Load, bytes(load.getOutput().getType())});
|
||||
} else if (auto vmm = dyn_cast<SpatVMMOp>(op)) {
|
||||
materialize(vmm.getInput(), sequence);
|
||||
auto weight = dyn_cast<RankedTensorType>(vmm.getWeight().getType());
|
||||
if (!weight || weight.getRank() != 2 || !weight.hasStaticShape() || !target.matrixColumns
|
||||
|| weight.getDimSize(1) <= 0
|
||||
|| static_cast<uint64_t>(weight.getDimSize(1)) % target.matrixColumns
|
||||
|| weight.getDimSize(0) <= 0
|
||||
|| static_cast<uint64_t>(weight.getDimSize(0)) > target.matrixRows
|
||||
|| weight.getEncoding() || !weight.getElementType().isIntOrFloat()
|
||||
|| weight.getElementTypeBitWidth() != 32) {
|
||||
error = "unavailable physical matrix weight geometry"; return false;
|
||||
}
|
||||
if (diagnostics && static_cast<uint64_t>(weight.getDimSize(0)) < target.matrixRows)
|
||||
++gaps["matrix input padding and copy forwarding"];
|
||||
uint64_t output = getPimMatrixOutputBytes(target, target.matrixCellBits,
|
||||
pim_binary::kMatrixWeightPrecisionBits, weight.getDimSize(1) / target.matrixColumns,
|
||||
ceil(target.computeBitwidth, 8));
|
||||
work(sequence, getPimMatrixWorkNs(target, target.computeBitwidth, output), Event::Matrix);
|
||||
} else if (isa<SpatVAddOp, SpatVSubOp, SpatVMulOp, SpatVMaxOp,
|
||||
SpatVVDMulOp, SpatVAvgOp, SpatReluOp, SpatSigmoidOp>(op)) {
|
||||
for (Value operand : op.getOperands()) materialize(operand, sequence);
|
||||
uint64_t width = ceil(target.computeBitwidth, 8);
|
||||
work(sequence, getPimVectorWorkNs(target, elements(op.getOperand(0).getType()),
|
||||
width, width, op.getNumOperands() == 2), Event::Vector);
|
||||
} else if (auto extract = dyn_cast<tensor::ExtractSliceOp>(op)) {
|
||||
auto place = storage(extract.getSource());
|
||||
if (place == Residence::Host || place == Residence::Local)
|
||||
if (!accessSlice(extract, sequence, place == Residence::Host)) return false;
|
||||
} else if (auto insert = dyn_cast<tensor::InsertSliceOp>(op)) {
|
||||
materialize(insert.getSource(), sequence);
|
||||
uint64_t size = bytes(insert.getSource().getType());
|
||||
work(sequence, localWork(size, true, true));
|
||||
if (compactStorage.contains(insert.getDest())) compactStorage.insert(insert.getResult());
|
||||
if (diagnostics) ++gaps["output copy forwarding"];
|
||||
} else if (auto transpose = dyn_cast<linalg::TransposeOp>(op)) {
|
||||
materialize(transpose.getInput(), sequence);
|
||||
auto type = cast<ShapedType>(transpose.getInput().getType());
|
||||
uint64_t volume = elements(type);
|
||||
if (!volume) {
|
||||
if (error.empty()) error = "zero-element transpose has no abstract timing contract";
|
||||
return false;
|
||||
}
|
||||
if (failed(computeRowMajorStridesChecked(type.getShape()))) {
|
||||
error = "abstract transpose geometry unavailable"; return false;
|
||||
}
|
||||
auto run = getTransposeRun(type.getShape(), transpose.getPermutation());
|
||||
uint64_t width = ceil(target.computeBitwidth, 8);
|
||||
uint64_t runBytes = mul(run.elements, width);
|
||||
uint64_t cost = run.sourceStride == 1
|
||||
? localWork(runBytes, true, true)
|
||||
: getPimVectorWorkNs(target, run.elements, width, width, false);
|
||||
work(sequence, mul(volume / run.elements, cost),
|
||||
run.sourceStride == 1 ? Event::LocalCopy : Event::VectorCopy);
|
||||
if (diagnostics) ++gaps["transpose alias and constant folding"];
|
||||
} else if (auto pad = dyn_cast<tensor::PadOp>(op)) {
|
||||
materialize(pad.getSource(), sequence);
|
||||
work(sequence, localWork(bytes(pad.getResult().getType()), false, true), Event::Fill);
|
||||
work(sequence, localWork(bytes(pad.getSource().getType()), true, true));
|
||||
if (diagnostics) ++gaps["padding fill and copy normalization"];
|
||||
} else if (auto concat = dyn_cast<SpatConcatOp>(op)) {
|
||||
for (Value input : concat.getInputs()) {
|
||||
materialize(input, sequence);
|
||||
uint64_t size = bytes(input.getType());
|
||||
work(sequence, localWork(size, true, true));
|
||||
}
|
||||
compactStorage.insert(concat.getOutput());
|
||||
if (diagnostics) ++gaps["concat alias and copy elimination"];
|
||||
} else if (auto empty = dyn_cast<tensor::EmptyOp>(op)) {
|
||||
compactStorage.insert(empty.getResult());
|
||||
} else if (isa<arith::ConstantOp, tensor::ExtractOp,
|
||||
tensor::CollapseShapeOp, tensor::ExpandShapeOp, tensor::CastOp,
|
||||
scf::YieldOp, SpatYieldOp>(op) || isPureIndexComputationOp(&op)) {
|
||||
// Compile-time shape/index work has no device execution cost.
|
||||
} else if (auto parallel = dyn_cast<SpatInParallelOp>(op)) {
|
||||
auto batch = parallel->getParentOfType<SpatScheduledComputeBatch>();
|
||||
auto first = batch ? batch.getOutputArgument(0) : std::nullopt;
|
||||
if (!first) { error = "unavailable abstract batch output mapping"; return false; }
|
||||
for (Operation &nested : parallel.getRegion().front()) {
|
||||
auto insert = dyn_cast<tensor::ParallelInsertSliceOp>(nested);
|
||||
auto destination = insert ? dyn_cast<BlockArgument>(insert.getDest()) : BlockArgument{};
|
||||
if (!destination || destination.getArgNumber() < first->getArgNumber()) {
|
||||
error = "unsupported abstract batch output"; return false;
|
||||
}
|
||||
unsigned result = destination.getArgNumber() - first->getArgNumber();
|
||||
if (result >= batch.getNumResults()) { error = "invalid abstract batch output"; return false; }
|
||||
if (succeeded(getDirectReturnOperandIndex(cast<OpResult>(batch.getResult(result))))) {
|
||||
materialize(insert.getSource(), sequence);
|
||||
sequence.push_back({Event::Store, bytes(insert.getSource().getType())});
|
||||
} else if (!batch.getResult(result).use_empty()) {
|
||||
error = "unavailable indirect batch output collection"; return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error = "unsupported abstract operation: " + op.getName().getStringRef().str();
|
||||
return false;
|
||||
}
|
||||
return error.empty();
|
||||
}
|
||||
|
||||
bool Extraction::block(Block &body, Sequence &sequence) {
|
||||
for (Operation &op : body) {
|
||||
if (++visited > 1000000) { error = "abstract extraction budget exceeded"; return false; }
|
||||
if (!operation(op, sequence)) return false;
|
||||
for (Value result : op.getResults())
|
||||
if (auto outputs = directOutputs.find(result); outputs != directOutputs.end())
|
||||
// Lowering inserts each store immediately after the shared producer.
|
||||
for (unsigned destination : llvm::reverse(outputs->second)) {
|
||||
(void)destination; // Distinct host destinations require distinct stores.
|
||||
sequence.push_back({Event::Store, bytes(result.getType())});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Extraction::core(Operation *op, int64_t physicalCore, unsigned lane) {
|
||||
if (physicalCore < 0 || physicalCore >= static_cast<int64_t>(program.size())) {
|
||||
loweringSupport = StaticLoweringSupport::Unsupported;
|
||||
error = "physical core ID is outside the target: " + std::to_string(physicalCore); return false;
|
||||
}
|
||||
if (!assignedCores.insert(physicalCore).second) {
|
||||
loweringSupport = StaticLoweringSupport::Unsupported;
|
||||
error = "duplicate physical core assignment: " + std::to_string(physicalCore); return false;
|
||||
}
|
||||
residence.clear(); publicationAccess.clear(); directOutputs.clear(); compactStorage.clear();
|
||||
environment.bindings.clear();
|
||||
ValueRange inputs, weights;
|
||||
auto single = dyn_cast<SpatScheduledCompute>(op);
|
||||
auto batch = dyn_cast<SpatScheduledComputeBatch>(op);
|
||||
if (single) { inputs = single.getInputs(); weights = single.getWeights(); }
|
||||
else { inputs = batch.getInputs(); weights = batch.getWeights(); environment.bindings[*batch.getLaneArgument()] = lane; }
|
||||
Sequence &sequence = program[physicalCore];
|
||||
if (single) {
|
||||
auto yield = cast<SpatYieldOp>(single.getBody().front().getTerminator());
|
||||
for (auto [result, value] : llvm::zip(single.getResults(), yield.getOperands()))
|
||||
if (auto destination = getDirectReturnOperandIndex(cast<OpResult>(result)); succeeded(destination)) {
|
||||
if (!value.getDefiningOp() || value.getDefiningOp()->getBlock() != &single.getBody().front()) {
|
||||
error = "unavailable direct output insertion position"; return false;
|
||||
}
|
||||
directOutputs[value].push_back(*destination);
|
||||
} else if (!result.use_empty()) {
|
||||
error = "unavailable indirect compute output collection"; return false;
|
||||
}
|
||||
}
|
||||
for (unsigned i = 0; i < weights.size(); ++i)
|
||||
residence[single ? *single.getWeightArgument(i) : *batch.getWeightArgument(i)] = Residence::CapturedWeight;
|
||||
for (unsigned i = 0; i < inputs.size(); ++i) {
|
||||
Value argument = single ? *single.getInputArgument(i) : *batch.getInputArgument(i);
|
||||
auto plan = planComputeInputMaterialization(op, i);
|
||||
if (failed(plan)) { error = "input materialization plan unavailable"; return false; }
|
||||
residence[argument] = storage(inputs[i]);
|
||||
// FuncOpArgToGlobalMemoryPattern removes this input and exposes the host
|
||||
// tensor in the body before entry-copy materialization runs.
|
||||
if (*plan == ComputeInputMaterialization::ForwardFunctionInput) {
|
||||
if (diagnostics) ++gaps["function-input whole-load versus slice materialization"];
|
||||
continue;
|
||||
}
|
||||
if (*plan == ComputeInputMaterialization::CopyAtEntry) {
|
||||
materialize(inputs[i], sequence);
|
||||
residence[argument] = Residence::Local;
|
||||
compactStorage.insert(argument);
|
||||
if (diagnostics) ++gaps["input host slicing and receive sinking"];
|
||||
}
|
||||
}
|
||||
return block(op->getRegion(0).front(), sequence);
|
||||
}
|
||||
|
||||
bool Extraction::build(ModuleOp module) {
|
||||
bool okay = true;
|
||||
module.walk<WalkOrder::PostOrder>([&](Operation *op) {
|
||||
if (indexed.contains(op) || isa<SpatChannelSendOp, SpatChannelReceiveOp,
|
||||
SpatSyncOp, SpatWaitOp, SpatHostStoreSyncOp, SpatHostWaitLoadOp, scf::IfOp>(op))
|
||||
if (Operation *parent = op->getParentOp()) indexed.insert(parent);
|
||||
});
|
||||
module.walk([&](Operation *op) {
|
||||
if (!okay) return WalkResult::interrupt();
|
||||
if (auto single = dyn_cast<SpatScheduledCompute>(op)) {
|
||||
auto coreId = op->getAttrOfType<IntegerAttr>("coreId");
|
||||
if (!coreId) { error = "missing physical core"; okay = false; }
|
||||
else okay = core(op, coreId.getInt(), 0);
|
||||
return WalkResult::skip();
|
||||
}
|
||||
if (auto batch = dyn_cast<SpatScheduledComputeBatch>(op)) {
|
||||
auto ids = op->getAttrOfType<DenseI32ArrayAttr>("coreIds");
|
||||
if (!ids || ids.size() != batch.getLaneCount()) { error = "missing physical batch cores"; okay = false; }
|
||||
else for (auto [lane, id] : llvm::enumerate(ids.asArrayRef())) if (!(okay = core(op, id, lane))) break;
|
||||
return WalkResult::skip();
|
||||
}
|
||||
return WalkResult::advance();
|
||||
});
|
||||
return okay && error.empty();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PimPeriodicEstimate onnx_mlir::evaluatePimSpatialSchedule(ModuleOp module,
|
||||
const PimInstructionTiming &target, PimPeriodicOptions options) try {
|
||||
if (!target.hasPeriodicTransferTiming() || !target.computeBitwidth)
|
||||
return unavailableCost("unsupported abstract timing contract");
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
Extraction extraction(target, options.workBreakdown);
|
||||
bool available = extraction.build(module);
|
||||
double extractionMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
PimPeriodicEstimate estimate;
|
||||
if (available) estimate = evaluatePimExecution(extraction.program, target, options);
|
||||
else {
|
||||
estimate.cost.reason = extraction.error;
|
||||
estimate.report = llvm::json::Object{{"status", "unavailable"}, {"reason", extraction.error}};
|
||||
if (options.verifyPeriodicState)
|
||||
for (auto &recurrence : estimate.cost.recurrence)
|
||||
recurrence.status = ScheduleRecurrenceEvidence::Unproved;
|
||||
}
|
||||
auto &report = estimate.report;
|
||||
auto &cost = estimate.cost;
|
||||
cost.timing.extractionSeconds = extractionMs / 1000;
|
||||
if (available || extraction.loweringSupport == StaticLoweringSupport::Unsupported)
|
||||
cost.loweringSupport = extraction.loweringSupport;
|
||||
if (available && options.workBreakdown) {
|
||||
PimPeriodicOptions sensitivity;
|
||||
sensitivity.ignoreLocalCopies = true;
|
||||
auto withoutCopies = analyzePimExecution(extraction.program, target, sensitivity);
|
||||
if (auto *queue = withoutCopies.getObject("memory_controller"))
|
||||
report["without_local_copy_work"] = std::move(*queue);
|
||||
}
|
||||
auto convergence = cost.convergenceProved();
|
||||
auto support = cost.loweringSupported();
|
||||
report["cost_available"] = cost.primary.has_value() && cost.alternative.has_value();
|
||||
report["convergence_proved"] = convergence
|
||||
? llvm::json::Value(*convergence) : llvm::json::Value(nullptr);
|
||||
report["lowering_supported"] = support
|
||||
? llvm::json::Value(*support) : llvm::json::Value(nullptr);
|
||||
std::string failureReason = std::move(cost.reason);
|
||||
cost.reason = "Spatial prediction accuracy has no formal bound";
|
||||
if (!cost.available()) cost.reason = "Spatial queue cost is unavailable";
|
||||
else if (cost.loweringSupport != StaticLoweringSupport::Supported)
|
||||
cost.reason = "Spatial lowering support is unproved";
|
||||
else if (convergence != true)
|
||||
cost.reason = "complete-state recurrence is unproved under one or both arbitration policies";
|
||||
if (!extraction.loweringReason.empty()) cost.reason = extraction.loweringReason;
|
||||
if (!failureReason.empty()) cost.reason = std::move(failureReason);
|
||||
report["lowering_support_scope"] = "static operation and transfer contract; final backend verification is required";
|
||||
if (!extraction.loweringReason.empty()) report["lowering_reason"] = extraction.loweringReason;
|
||||
report["safe_for_selection"] = false;
|
||||
report["diagnostic_only"] = true;
|
||||
report["error_bound_available"] = false;
|
||||
report["extraction_ms"] = extractionMs;
|
||||
report["visited_operations"] = extraction.visited;
|
||||
if (!options.workBreakdown) return estimate;
|
||||
report["constant_load_sites"] = extraction.constantLoads;
|
||||
uint64_t workNs = 0, records = 0, constantRequests = 0, constantBytes = 0;
|
||||
constexpr const char *workClassNames[] = {
|
||||
"matrix", "vector", "local_copy", "fill", "vector_copy", "scalar_issue"};
|
||||
std::array<uint64_t, std::size(workClassNames)> workByClass{};
|
||||
static_assert(Event::ScalarIssue + 1 == std::size(workClassNames));
|
||||
std::array<uint64_t, 7> counts{};
|
||||
std::map<uint64_t, uint64_t> readSizes;
|
||||
std::function<void(const Sequence &, uint64_t)> summarize =
|
||||
[&](const Sequence &sequence, uint64_t multiplier) {
|
||||
for (const Event &event : sequence) {
|
||||
++records;
|
||||
if (event.kind == Event::Repeat) summarize(event.body, mul(multiplier, event.amount));
|
||||
else {
|
||||
counts[event.kind] = add(counts[event.kind], multiplier);
|
||||
if (event.kind == Event::Work) {
|
||||
workNs = add(workNs, mul(multiplier, event.amount));
|
||||
switch (event.workClass) {
|
||||
case Event::Matrix:
|
||||
case Event::Vector:
|
||||
case Event::LocalCopy:
|
||||
case Event::Fill:
|
||||
case Event::VectorCopy:
|
||||
case Event::ScalarIssue:
|
||||
workByClass[event.workClass] = add(workByClass[event.workClass], mul(multiplier, event.amount));
|
||||
break;
|
||||
default: throw pim_timing::Unrepresentable{"unsupported execution work class"};
|
||||
}
|
||||
} else if (event.kind == Event::Load) {
|
||||
readSizes[event.amount] = add(readSizes[event.amount], multiplier);
|
||||
if (event.constant) {
|
||||
constantRequests = add(constantRequests, multiplier);
|
||||
constantBytes = add(constantBytes, mul(multiplier, event.amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const Sequence &sequence : extraction.program) summarize(sequence, 1);
|
||||
report["compute_and_local_work_ns"] = workNs;
|
||||
llvm::json::Array classes;
|
||||
for (uint64_t value : workByClass) classes.push_back(value);
|
||||
llvm::json::Array columns;
|
||||
for (const char *name : workClassNames) columns.push_back(name);
|
||||
report["work_class_columns"] = std::move(columns);
|
||||
report["work_class_ns"] = std::move(classes);
|
||||
llvm::json::Object eventCounts;
|
||||
const char *names[] = {"work_chunks", "load", "store", "send", "receive", "sync", "wait"};
|
||||
for (size_t i = 0; i < counts.size(); ++i) eventCounts[names[i]] = counts[i];
|
||||
report["event_counts"] = std::move(eventCounts);
|
||||
report["compact_records"] = records;
|
||||
report["constant_read_requests"] = constantRequests;
|
||||
report["constant_read_bytes"] = constantBytes;
|
||||
llvm::json::Array sizes;
|
||||
for (auto [bytes, count] : readSizes)
|
||||
sizes.push_back(llvm::json::Object{{"bytes", bytes}, {"requests", count}});
|
||||
report["read_sizes"] = std::move(sizes);
|
||||
llvm::json::Object gaps;
|
||||
for (const auto &[name, count] : extraction.gaps) gaps[name] = count;
|
||||
report["unresolved_decisions"] = std::move(gaps);
|
||||
report["omitted_effects"] = llvm::json::Array{"scalar register setup", "indirect output collection",
|
||||
"host slice splitting and folding", "copy forwarding and local reuse legality"};
|
||||
report["analysis_ms"] = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
return estimate;
|
||||
} catch (const pim_timing::Unrepresentable &failure) {
|
||||
return unavailableCost(failure.reason);
|
||||
}
|
||||
|
||||
llvm::json::Object onnx_mlir::analyzePimSpatialSchedule(ModuleOp module,
|
||||
const PimInstructionTiming &target, PimPeriodicOptions options) {
|
||||
return evaluatePimSpatialSchedule(module, target, options).report;
|
||||
}
|
||||
|
||||
void onnx_mlir::configurePimScheduleCosts(spatial::SchedulingTarget &target,
|
||||
PimInstructionTiming timing) {
|
||||
auto contract = getPimPeriodicTimingContract();
|
||||
if (contract) timing.matrixCellBits = *contract;
|
||||
auto hardware = std::make_shared<const PimInstructionTiming>(std::move(timing));
|
||||
target.reportPeriodicSchedule = [hardware](const spatial::ComputeGraph &graph,
|
||||
const spatial::ScheduledComputeMaterializationResult &materialization,
|
||||
const spatial::DeferredTransferPlan &transfers,
|
||||
const spatial::DeferredBoundaryPlan &boundaries) {
|
||||
dumpPeriodicScheduleEstimate(graph, materialization, transfers, boundaries, *hardware);
|
||||
};
|
||||
if (!contract) target.wholeScheduleCostUnavailable = llvm::toString(contract.takeError());
|
||||
if (!hardware->hasPeriodicTransferTiming())
|
||||
target.wholeScheduleCostUnavailable = "target has no supported queue timing contract";
|
||||
if (pimDisableSynchronization)
|
||||
target.wholeScheduleCostUnavailable = "synchronization ablation has no periodic queue timing contract";
|
||||
if (!target.wholeScheduleCostUnavailable.empty()) return;
|
||||
target.estimateRealizedScheduleCost = [hardware](mlir::ModuleOp module,
|
||||
llvm::json::Object &summary) {
|
||||
PimPeriodicOptions options;
|
||||
options.workBreakdown = pimPeriodicDiagnostics;
|
||||
options.measureTime = true;
|
||||
options.verifyPeriodicState = true;
|
||||
auto estimate = evaluatePimSpatialSchedule(module, *hardware, options);
|
||||
auto cost = std::move(estimate.cost);
|
||||
summary = std::move(estimate.report);
|
||||
if (estimate.queue)
|
||||
summary["interval_source"] = cost.convergenceProved() == true
|
||||
? "complete-state recurrence under both arbitration policies"
|
||||
: "recurrence is unproved under one or both arbitration policies";
|
||||
summary["cost_available"] = cost.available();
|
||||
return cost;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "PimPeriodicAnalysis.hpp"
|
||||
#include "mlir/IR/BuiltinOps.h"
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
void configurePimScheduleCosts(spatial::SchedulingTarget &target, PimInstructionTiming timing);
|
||||
|
||||
// Read-only, target-specific work extraction. No target dialect is constructed.
|
||||
PimPeriodicEstimate evaluatePimSpatialSchedule(mlir::ModuleOp module,
|
||||
const PimInstructionTiming &target, PimPeriodicOptions options = {});
|
||||
llvm::json::Object analyzePimSpatialSchedule(mlir::ModuleOp module,
|
||||
const PimInstructionTiming &target, PimPeriodicOptions options = {});
|
||||
|
||||
} // namespace onnx_mlir
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace onnx_mlir::pim_timing {
|
||||
|
||||
struct Unrepresentable { const char *reason; };
|
||||
|
||||
// Optional timing queries must fail without wrapping or terminating compilation.
|
||||
struct Arithmetic {
|
||||
static constexpr uint64_t limit = std::numeric_limits<int64_t>::max();
|
||||
unsigned long long add(uint64_t a, uint64_t b) const {
|
||||
if (a > limit || b > limit - a) throw Unrepresentable{"timing addition overflow"};
|
||||
return a + b;
|
||||
}
|
||||
unsigned long long multiply(uint64_t a, uint64_t b) const {
|
||||
if (a > limit || b > limit || (a && b > limit / a))
|
||||
throw Unrepresentable{"timing multiplication overflow"};
|
||||
return a * b;
|
||||
}
|
||||
unsigned long long subtract(uint64_t a, uint64_t b) const {
|
||||
if (a > limit || b > a) throw Unrepresentable{"timing subtraction underflow"};
|
||||
return a - b;
|
||||
}
|
||||
unsigned long long divideUp(uint64_t a, uint64_t b) const {
|
||||
if (!b || a > limit || b > limit) throw Unrepresentable{"unrepresentable timing division"};
|
||||
return a / b + (a % b != 0);
|
||||
}
|
||||
};
|
||||
inline constexpr Arithmetic arithmetic{};
|
||||
inline uint64_t add(uint64_t a, uint64_t b) { return arithmetic.add(a, b); }
|
||||
inline uint64_t mul(uint64_t a, uint64_t b) { return arithmetic.multiply(a, b); }
|
||||
inline uint64_t sub(uint64_t a, uint64_t b) { return arithmetic.subtract(a, b); }
|
||||
inline uint64_t ceil(uint64_t a, uint64_t b) { return arithmetic.divideUp(a, b); }
|
||||
|
||||
} // namespace onnx_mlir::pim_timing
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
|
||||
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/Common.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/ComputeLikeRegionUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
|
||||
|
||||
using namespace mlir;
|
||||
@@ -34,15 +35,6 @@ static bool isUsedOnlyByExtractSlices(Value value) {
|
||||
&& llvm::all_of(value.getUsers(), [](Operation* user) { return isa<tensor::ExtractSliceOp>(user); });
|
||||
}
|
||||
|
||||
static FailureOr<unsigned> getDirectReturnOperandIndex(OpResult result) {
|
||||
if (!result.hasOneUse())
|
||||
return failure();
|
||||
|
||||
auto returnOp = dyn_cast<func::ReturnOp>(*result.getUsers().begin());
|
||||
if (!returnOp)
|
||||
return failure();
|
||||
return result.getUses().begin()->getOperandNumber();
|
||||
}
|
||||
|
||||
static FailureOr<SmallVector<FragmentAssemblyCopy, 8>>
|
||||
collectFragmentAssemblyCopiesFromBlueprint(spatial::SpatBlueprintOp blueprint,
|
||||
@@ -85,7 +77,9 @@ collectFragmentAssemblyCopiesFromBlueprint(spatial::SpatBlueprintOp blueprint,
|
||||
flatStrides)))
|
||||
return failure();
|
||||
|
||||
SmallVector<int64_t> hostStrides = computeRowMajorStrides(resultType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(resultType.getShape());
|
||||
if (failed(checkedStrides)) return failure();
|
||||
SmallVector<int64_t> hostStrides = std::move(*checkedStrides);
|
||||
for (int64_t fragmentIndex = 0; fragmentIndex < static_cast<int64_t>(operandIndices.size()); ++fragmentIndex) {
|
||||
Value source = fixedSource ? fixedSource : mapper.lookupOrDefault(fragmentOperands[operandIndices[fragmentIndex]]);
|
||||
auto sourceType = dyn_cast<RankedTensorType>(source.getType());
|
||||
@@ -183,7 +177,9 @@ collectTopLevelFragmentAssemblyCopies(OpResult result, RankedTensorType packedRe
|
||||
flatSizes,
|
||||
flatStrides)))
|
||||
return failure();
|
||||
SmallVector<int64_t> hostStrides = computeRowMajorStrides(hostResultType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(hostResultType.getShape());
|
||||
if (failed(checkedStrides)) return failure();
|
||||
SmallVector<int64_t> hostStrides = std::move(*checkedStrides);
|
||||
for (int64_t fragmentIndex = 0; fragmentIndex < static_cast<int64_t>(operandIndices.size()); ++fragmentIndex) {
|
||||
if (operandIndices[fragmentIndex] != static_cast<int64_t>(use.getOperandNumber()))
|
||||
continue;
|
||||
@@ -239,14 +235,16 @@ static Value createScaledIndexValue(IRRewriter& rewriter, Location loc, Value ba
|
||||
return arith::MulIOp::create(rewriter, loc, base, scaleValue).getResult();
|
||||
}
|
||||
|
||||
static Value createHostTargetOffset(IRRewriter& rewriter,
|
||||
static FailureOr<Value> createHostTargetOffset(IRRewriter& rewriter,
|
||||
Location loc,
|
||||
ShapedType destinationType,
|
||||
ArrayRef<OpFoldResult> mixedOffsets,
|
||||
ArrayRef<int64_t> additionalOffsets,
|
||||
IRMapping& mapper) {
|
||||
int64_t elementBytes = static_cast<int64_t>(getElementTypeSizeInBytes(destinationType.getElementType()));
|
||||
SmallVector<int64_t> strides = computeRowMajorStrides(destinationType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(destinationType.getShape());
|
||||
if (failed(checkedStrides)) return failure();
|
||||
SmallVector<int64_t> strides = std::move(*checkedStrides);
|
||||
|
||||
Value totalOffset;
|
||||
for (auto [dim, offset] : llvm::enumerate(mixedOffsets)) {
|
||||
@@ -277,7 +275,7 @@ static Value createHostTargetOffset(IRRewriter& rewriter,
|
||||
return totalOffset;
|
||||
}
|
||||
|
||||
static Value createHostTargetOffset(IRRewriter& rewriter,
|
||||
static FailureOr<Value> createHostTargetOffset(IRRewriter& rewriter,
|
||||
tensor::ParallelInsertSliceOp insertSlice,
|
||||
ShapedType destinationType,
|
||||
IRMapping& mapper) {
|
||||
@@ -380,6 +378,9 @@ LogicalResult raptor::SpatialToPimPass::lowerComputeBatchOp(spatial::SpatSchedul
|
||||
mapper.map(*oldWeightArg, coreBatchOp.getWeightArgument(weightIndex));
|
||||
}
|
||||
for (unsigned inputIndex = 0; inputIndex < computeBatchOp.getInputs().size(); ++inputIndex) {
|
||||
auto inputPlan = planComputeInputMaterialization(computeBatchOp, inputIndex);
|
||||
if (failed(inputPlan) || *inputPlan != ComputeInputMaterialization::CopyAtEntry)
|
||||
return computeBatchOp.emitOpError("expected body-entry compute_batch input materialization");
|
||||
auto oldArg = computeBatchOp.getInputArgument(inputIndex);
|
||||
if (!oldArg)
|
||||
return computeBatchOp.emitOpError("expected compute_batch input block arguments before lowering");
|
||||
@@ -491,19 +492,20 @@ LogicalResult raptor::SpatialToPimPass::lowerComputeBatchOp(spatial::SpatSchedul
|
||||
if (failed(fragmentAssemblyRuns))
|
||||
return failure();
|
||||
SmallVector<int64_t> zeroOffsets(hostTargetType.getRank(), 0);
|
||||
Value baseHostOffset = createHostTargetOffset(rewriter,
|
||||
auto baseHostOffset = createHostTargetOffset(rewriter,
|
||||
blueprint.getLoc(),
|
||||
hostTargetType,
|
||||
insertSlice.getMixedOffsets(),
|
||||
zeroOffsets,
|
||||
mapper);
|
||||
if (failed(baseHostOffset)) return failure();
|
||||
FailureOr<Value> updatedHostTarget = emitFragmentAssemblyCopyRuns(rewriter,
|
||||
blueprint.getLoc(),
|
||||
*fragmentAssemblyRuns,
|
||||
hostTarget,
|
||||
coreBatchOp.getOperation(),
|
||||
std::nullopt,
|
||||
baseHostOffset);
|
||||
*baseHostOffset);
|
||||
if (failed(updatedHostTarget))
|
||||
return failure();
|
||||
hostOutputTensors[resultIndex] = *updatedHostTarget;
|
||||
@@ -511,7 +513,8 @@ LogicalResult raptor::SpatialToPimPass::lowerComputeBatchOp(spatial::SpatSchedul
|
||||
}
|
||||
}
|
||||
|
||||
Value hostTargetOffset = createHostTargetOffset(rewriter, insertSlice, hostTargetType, mapper);
|
||||
auto hostTargetOffset = createHostTargetOffset(rewriter, insertSlice, hostTargetType, mapper);
|
||||
if (failed(hostTargetOffset)) return failure();
|
||||
Value zeroOffset = getOrCreateIndexConstant(rewriter, coreBatchOp.getOperation(), 0);
|
||||
auto sizeAttr = getTensorSizeInBytesAttr(rewriter, coreBatchOp.getOperation(), mappedSource);
|
||||
if (failed(sizeAttr))
|
||||
@@ -519,7 +522,7 @@ LogicalResult raptor::SpatialToPimPass::lowerComputeBatchOp(spatial::SpatSchedul
|
||||
pim::PimMemCopyDevToHostOp::create(rewriter,
|
||||
insertSlice.getLoc(),
|
||||
hostTarget.getType(),
|
||||
hostTargetOffset,
|
||||
*hostTargetOffset,
|
||||
zeroOffset,
|
||||
hostTarget,
|
||||
mappedSource,
|
||||
|
||||
@@ -123,7 +123,7 @@ static bool isLoopCarriedOutput(mlir::Value operand, Operation* operation) {
|
||||
.getOperand(argument.getArgNumber() - 1) == operation->getResult(0);
|
||||
}
|
||||
|
||||
mlir::Value getBestOutputTensorFromOperandsOrAllocate(RewriterBase& rewriter, Operation* operation) {
|
||||
mlir::Value getReusableOutputTensor(Operation* operation) {
|
||||
assert("Only support operations with a single result" && operation->getNumResults() == 1);
|
||||
mlir::Value result = operation->getResult(0);
|
||||
auto resultType = result.getType();
|
||||
@@ -141,8 +141,13 @@ mlir::Value getBestOutputTensorFromOperandsOrAllocate(RewriterBase& rewriter, Op
|
||||
|
||||
if (bestOperand != validOperands.end())
|
||||
return *bestOperand;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto resultShapedType = cast<ShapedType>(resultType);
|
||||
mlir::Value getBestOutputTensorFromOperandsOrAllocate(RewriterBase& rewriter, Operation* operation) {
|
||||
if (mlir::Value operand = getReusableOutputTensor(operation))
|
||||
return operand;
|
||||
auto resultShapedType = cast<ShapedType>(operation->getResult(0).getType());
|
||||
rewriter.setInsertionPoint(operation);
|
||||
return tensor::EmptyOp::create(
|
||||
rewriter, operation->getLoc(), resultShapedType.getShape(), resultShapedType.getElementType());
|
||||
|
||||
@@ -47,6 +47,10 @@ mlir::Operation* getEarliestUserWithinBlock(mlir::Value value);
|
||||
|
||||
mlir::SmallVector<mlir::Value> getOpOperandsSortedByUses(mlir::Operation* operation);
|
||||
|
||||
// Destination selection only. Bufferization still proves physical aliasing.
|
||||
// Valid for the current SSA graph only; recompute after use or order changes.
|
||||
mlir::Value getReusableOutputTensor(mlir::Operation* operation);
|
||||
|
||||
mlir::Value getBestOutputTensorFromOperandsOrAllocate(mlir::RewriterBase& rewriter, mlir::Operation* operation);
|
||||
|
||||
mlir::LogicalResult validateFragmentAssemblyMetadata(onnx_mlir::spatial::SpatBlueprintOp blueprint,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <cassert>
|
||||
#include "mlir/Dialect/Arith/IR/Arith.h"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/ComputeLikeRegionUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
|
||||
@@ -7,6 +9,41 @@ using namespace mlir;
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
FailureOr<unsigned> getDirectReturnOperandIndex(OpResult result) {
|
||||
if (!result.hasOneUse())
|
||||
return failure();
|
||||
|
||||
auto returnOp = dyn_cast<func::ReturnOp>(*result.getUsers().begin());
|
||||
if (!returnOp)
|
||||
return failure();
|
||||
return result.getUses().begin()->getOperandNumber();
|
||||
}
|
||||
|
||||
|
||||
FailureOr<ComputeInputMaterialization>
|
||||
planComputeInputMaterialization(Operation* owner, unsigned inputIndex) {
|
||||
Value input;
|
||||
bool batched = false;
|
||||
if (auto compute = dyn_cast<spatial::SpatScheduledCompute>(owner)) {
|
||||
if (inputIndex >= compute.getInputs().size() || !compute.getInputArgument(inputIndex))
|
||||
return failure();
|
||||
if (compute.getInputArgument(inputIndex)->use_empty())
|
||||
return ComputeInputMaterialization::Unused;
|
||||
input = compute.getInputs()[inputIndex];
|
||||
} else if (auto batch = dyn_cast<spatial::SpatScheduledComputeBatch>(owner)) {
|
||||
if (inputIndex >= batch.getInputs().size() || !batch.getInputArgument(inputIndex))
|
||||
return failure();
|
||||
input = batch.getInputs()[inputIndex];
|
||||
batched = true;
|
||||
} else return failure();
|
||||
if (auto argument = dyn_cast<BlockArgument>(input); argument
|
||||
&& isa<func::FuncOp>(argument.getOwner()->getParentOp()))
|
||||
return ComputeInputMaterialization::ForwardFunctionInput;
|
||||
if (!batched && input.getDefiningOp<arith::ConstantOp>())
|
||||
return ComputeInputMaterialization::ForwardConstant;
|
||||
return ComputeInputMaterialization::CopyAtEntry;
|
||||
}
|
||||
|
||||
std::optional<unsigned> getDirectComputeLikeInputIndex(Operation* owner, unsigned operandNumber) {
|
||||
auto getInputIndex = [operandNumber](Operation* op, unsigned inputCount) -> std::optional<unsigned> {
|
||||
if (inputCount == 0)
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
|
||||
namespace onnx_mlir {
|
||||
|
||||
// Input placement only; bufferization and folding determine physical requests.
|
||||
enum class ComputeInputMaterialization { Unused, ForwardConstant, ForwardFunctionInput, CopyAtEntry };
|
||||
|
||||
mlir::FailureOr<unsigned> getDirectReturnOperandIndex(mlir::OpResult result);
|
||||
|
||||
mlir::FailureOr<ComputeInputMaterialization>
|
||||
planComputeInputMaterialization(mlir::Operation* owner, unsigned inputIndex);
|
||||
|
||||
std::optional<unsigned> getDirectComputeLikeInputIndex(mlir::Operation* owner, unsigned operandNumber);
|
||||
|
||||
void replaceAndEraseDirectComputeLikeInput(mlir::PatternRewriter& rewriter,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
|
||||
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/Common.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/ComputeLikeRegionUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
|
||||
#include "src/Dialect/ONNX/ONNXOps.hpp"
|
||||
|
||||
@@ -80,7 +81,9 @@ static FailureOr<Value> lowerFragmentAssemblyBlueprint(IRRewriter& rewriter,
|
||||
return blueprint.emitOpError("contiguous row-major fragment storage does not match its logical result"), failure();
|
||||
return *reshaped;
|
||||
}
|
||||
SmallVector<int64_t> hostStrides = computeRowMajorStrides(resultType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(resultType.getShape());
|
||||
if (failed(checkedStrides)) return failure();
|
||||
SmallVector<int64_t> hostStrides = std::move(*checkedStrides);
|
||||
SmallVector<FragmentAssemblyCopy, 8> copies;
|
||||
for (int64_t fragmentIndex = 0; fragmentIndex < static_cast<int64_t>(operandIndices.size()); ++fragmentIndex) {
|
||||
int64_t operandIndex = operandIndices[fragmentIndex];
|
||||
@@ -430,10 +433,16 @@ LogicalResult raptor::SpatialToPimPass::lowerComputeOp(spatial::SpatScheduledCom
|
||||
auto blockArg = computeOp.getInputArgument(inputIndex);
|
||||
if (!blockArg)
|
||||
return computeOp.emitOpError("expected compute input block arguments during input materialization");
|
||||
if (blockArg->use_empty())
|
||||
auto inputPlan = planComputeInputMaterialization(computeOp, inputIndex);
|
||||
if (failed(inputPlan))
|
||||
return computeOp.emitOpError("failed to plan compute input materialization");
|
||||
if (*inputPlan == ComputeInputMaterialization::Unused)
|
||||
continue;
|
||||
if (*inputPlan == ComputeInputMaterialization::ForwardFunctionInput)
|
||||
return computeOp.emitOpError("function inputs must be forwarded before core input materialization");
|
||||
|
||||
if (auto constantOp = input.getDefiningOp<arith::ConstantOp>()) {
|
||||
if (*inputPlan == ComputeInputMaterialization::ForwardConstant) {
|
||||
auto constantOp = input.getDefiningOp<arith::ConstantOp>();
|
||||
blockArg->replaceAllUsesWith(getOrCreateConstantLike(constantFolder, constantOp));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@ static void copyRaptorDebugAttrs(Operation* source, Operation* target) {
|
||||
}
|
||||
}
|
||||
|
||||
static Value createDestinationByteOffset(PatternRewriter& rewriter,
|
||||
static FailureOr<Value> createDestinationByteOffset(PatternRewriter& rewriter,
|
||||
tensor::InsertSliceOp insert) {
|
||||
auto destinationType = cast<RankedTensorType>(insert.getDestType());
|
||||
SmallVector<int64_t> strides = computeRowMajorStrides(destinationType.getShape());
|
||||
auto checkedStrides = computeRowMajorStridesChecked(destinationType.getShape());
|
||||
if (failed(checkedStrides)) return failure();
|
||||
SmallVector<int64_t> strides = std::move(*checkedStrides);
|
||||
int64_t elementBytes = getElementTypeSizeInBytes(destinationType.getElementType());
|
||||
Value total = arith::ConstantIndexOp::create(rewriter, insert.getLoc(), 0);
|
||||
for (auto [dimension, offset] : llvm::enumerate(insert.getMixedOffsets())) {
|
||||
@@ -114,9 +116,10 @@ static LogicalResult lowerReceive(
|
||||
}
|
||||
|
||||
rewriter.setInsertionPoint(destinationInsert);
|
||||
Value targetOffset = createDestinationByteOffset(rewriter, destinationInsert);
|
||||
auto targetOffset = createDestinationByteOffset(rewriter, destinationInsert);
|
||||
if (failed(targetOffset)) return failure();
|
||||
auto copy = pim::PimMemCopyOp::create(
|
||||
rewriter, op.getLoc(), destinationInsert.getDestType(), targetOffset, zero,
|
||||
rewriter, op.getLoc(), destinationInsert.getDestType(), *targetOffset, zero,
|
||||
destinationInsert.getDest(), *received, *sizeAttr);
|
||||
rewriter.replaceOp(destinationInsert, copy.getOutput());
|
||||
rewriter.eraseOp(op);
|
||||
|
||||
@@ -179,29 +179,18 @@ struct FuncOpArgToGlobalMemoryPattern final : OpRewritePattern<mlir::func::FuncO
|
||||
|
||||
for (auto& argUses : llvm::make_early_inc_range(arg.getUses())) {
|
||||
auto argUser = argUses.getOwner();
|
||||
if (auto spatCompute = dyn_cast<spatial::SpatScheduledCompute>(argUser)) {
|
||||
auto inputIndex = getDirectComputeLikeInputIndex(spatCompute, argUses.getOperandNumber());
|
||||
if (!inputIndex)
|
||||
if (isa<spatial::SpatScheduledCompute, spatial::SpatScheduledComputeBatch>(argUser)) {
|
||||
auto inputIndex = getDirectComputeLikeInputIndex(argUser, argUses.getOperandNumber());
|
||||
if (!inputIndex) return failure();
|
||||
auto plan = planComputeInputMaterialization(argUser, *inputIndex);
|
||||
if (failed(plan) || (*plan != ComputeInputMaterialization::ForwardFunctionInput
|
||||
&& *plan != ComputeInputMaterialization::Unused))
|
||||
return failure();
|
||||
auto BBArgIndex = *inputIndex;
|
||||
rewriter.setInsertionPoint(&spatCompute.getBody().front().front());
|
||||
rewriter.setInsertionPoint(&argUser->getRegion(0).front().front());
|
||||
auto getGlobalOp = memref::GetGlobalOp::create(rewriter, loc, memRefType, argName);
|
||||
auto toTensor = bufferization::ToTensorOp::create(
|
||||
rewriter, loc, argRankedTensorType, getGlobalOp, rewriter.getUnitAttr(), rewriter.getUnitAttr());
|
||||
|
||||
replaceAndEraseDirectComputeLikeInput(rewriter, spatCompute.getOperation(), BBArgIndex, toTensor);
|
||||
}
|
||||
else if (auto spatComputeBatch = dyn_cast<spatial::SpatScheduledComputeBatch>(argUser)) {
|
||||
auto inputIndex = getDirectComputeLikeInputIndex(spatComputeBatch, argUses.getOperandNumber());
|
||||
if (!inputIndex)
|
||||
return failure();
|
||||
auto BBArgIndex = *inputIndex;
|
||||
rewriter.setInsertionPoint(&spatComputeBatch.getBody().front().front());
|
||||
auto getGlobalOp = memref::GetGlobalOp::create(rewriter, loc, memRefType, argName);
|
||||
auto toTensor = bufferization::ToTensorOp::create(
|
||||
rewriter, loc, argRankedTensorType, getGlobalOp, rewriter.getUnitAttr(), rewriter.getUnitAttr());
|
||||
|
||||
replaceAndEraseDirectComputeLikeInput(rewriter, spatComputeBatch.getOperation(), BBArgIndex, toTensor);
|
||||
replaceAndEraseDirectComputeLikeInput(rewriter, argUser, *inputIndex, toTensor);
|
||||
}
|
||||
else {
|
||||
rewriter.setInsertionPoint(argUser);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "llvm/Support/MathExtras.h"
|
||||
|
||||
#include "ContiguityPatterns.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/CopyAccessPlan.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
|
||||
@@ -84,17 +85,6 @@ static FailureOr<int64_t> checkedPositiveMul(int64_t lhs, int64_t rhs) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static FailureOr<int64_t> checkedPositiveProduct(ArrayRef<int64_t> values) {
|
||||
int64_t result = 1;
|
||||
for (int64_t value : values) {
|
||||
auto product = checkedPositiveMul(result, value);
|
||||
if (failed(product))
|
||||
return failure();
|
||||
result = *product;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static FailureOr<SmallVector<int64_t>> getStaticMemRefStrides(MemRefType type) {
|
||||
SmallVector<int64_t> strides;
|
||||
int64_t offset = 0;
|
||||
@@ -348,12 +338,9 @@ inferLogicalCopyShape(MemRefType targetType, MemRefType sourceType, int64_t size
|
||||
}
|
||||
|
||||
static FailureOr<int64_t> getContiguousSuffixRank(
|
||||
Value value, ArrayRef<int64_t> copyShape, Type elementType = {}) {
|
||||
Value value, ArrayRef<int64_t> copyShape) {
|
||||
auto type = dyn_cast<MemRefType>(value.getType());
|
||||
if (type && elementType && isPackedByteBuffer(type))
|
||||
return copyShape.size();
|
||||
if (!type || !type.hasStaticShape() || !hasByteSizedElementType(type.getElementType())
|
||||
|| (elementType && type.getElementType() != elementType)
|
||||
|| type.getRank() != static_cast<int64_t>(copyShape.size()))
|
||||
return failure();
|
||||
if (llvm::any_of(copyShape, [](int64_t dim) { return dim <= 0; }))
|
||||
@@ -363,42 +350,19 @@ static FailureOr<int64_t> getContiguousSuffixRank(
|
||||
if (failed(strides))
|
||||
return failure();
|
||||
|
||||
int64_t expectedStride = 1;
|
||||
int64_t contiguousSuffixRank = 0;
|
||||
for (int64_t dim = type.getRank() - 1; dim >= 0; --dim) {
|
||||
if (copyShape[dim] != 1 && (*strides)[dim] != expectedStride)
|
||||
break;
|
||||
++contiguousSuffixRank;
|
||||
auto nextStride = checkedPositiveMul(expectedStride, copyShape[dim]);
|
||||
if (failed(nextStride))
|
||||
return failure();
|
||||
expectedStride = *nextStride;
|
||||
}
|
||||
return contiguousSuffixRank;
|
||||
return onnx_mlir::getContiguousSuffixRank(copyShape, *strides);
|
||||
}
|
||||
|
||||
static FailureOr<SmallVector<int64_t>> getOuterByteStrides(
|
||||
Value value, const LogicalCopyShape ©Shape, size_t outerRank) {
|
||||
static FailureOr<SmallVector<int64_t>> getCopyElementStrides(
|
||||
Value value, const LogicalCopyShape ©Shape) {
|
||||
auto type = cast<MemRefType>(value.getType());
|
||||
SmallVector<int64_t> strides;
|
||||
if (isPackedByteBuffer(type))
|
||||
strides = computeRowMajorStrides(copyShape.dimensions);
|
||||
else {
|
||||
auto proven = getProvenMemRefStrides(value);
|
||||
if (failed(proven))
|
||||
return failure();
|
||||
strides = std::move(*proven);
|
||||
}
|
||||
int64_t elementByteWidth = static_cast<int64_t>(
|
||||
getElementTypeSizeInBytes(copyShape.elementType));
|
||||
SmallVector<int64_t> result;
|
||||
for (int64_t stride : ArrayRef<int64_t>(strides).take_front(outerRank)) {
|
||||
auto byteStride = checkedPositiveMul(stride, elementByteWidth);
|
||||
if (failed(byteStride))
|
||||
return failure();
|
||||
result.push_back(*byteStride);
|
||||
}
|
||||
return result;
|
||||
return computeRowMajorStridesChecked(copyShape.dimensions);
|
||||
if (!hasByteSizedElementType(type.getElementType())
|
||||
|| type.getElementType() != copyShape.elementType
|
||||
|| type.getRank() != static_cast<int64_t>(copyShape.dimensions.size()))
|
||||
return failure();
|
||||
return getProvenMemRefStrides(value);
|
||||
}
|
||||
|
||||
static FailureOr<CopyEndpointPlan> analyzeCopyEndpoint(Value value, Value initialByteOffset, MemRefType logicalType) {
|
||||
@@ -498,20 +462,19 @@ analyzeCopyRewrite(Value target, Value source, Value targetOffset, Value sourceO
|
||||
if (failed(logicalCopyShape))
|
||||
return failure();
|
||||
|
||||
auto targetSuffixRank = getContiguousSuffixRank(
|
||||
target, logicalCopyShape->dimensions, logicalCopyShape->elementType);
|
||||
auto sourceSuffixRank = getContiguousSuffixRank(
|
||||
source, logicalCopyShape->dimensions, logicalCopyShape->elementType);
|
||||
if (failed(targetSuffixRank) || failed(sourceSuffixRank))
|
||||
auto targetStrides = getCopyElementStrides(target, *logicalCopyShape);
|
||||
auto sourceStrides = getCopyElementStrides(source, *logicalCopyShape);
|
||||
if (failed(targetStrides) || failed(sourceStrides))
|
||||
return failure();
|
||||
auto access = CopyAccessPlan::build(
|
||||
logicalCopyShape->dimensions, *sourceStrides, *targetStrides);
|
||||
if (failed(access))
|
||||
return failure();
|
||||
|
||||
CopyRewritePlan plan;
|
||||
plan.target = *targetPlan;
|
||||
plan.source = *sourcePlan;
|
||||
|
||||
int64_t contiguousSuffixRank = std::min(*targetSuffixRank, *sourceSuffixRank);
|
||||
if (contiguousSuffixRank
|
||||
== static_cast<int64_t>(logicalCopyShape->dimensions.size())) {
|
||||
if (access->getOuterShape().empty()) {
|
||||
plan.kind = CopyRewritePlan::Kind::Direct;
|
||||
plan.directBytes = size;
|
||||
return plan;
|
||||
@@ -522,30 +485,22 @@ analyzeCopyRewrite(Value target, Value source, Value targetOffset, Value sourceO
|
||||
plan.kind = CopyRewritePlan::Kind::Loop;
|
||||
plan.loop.targetBaseOffset = plan.target.offset;
|
||||
plan.loop.sourceBaseOffset = plan.source.offset;
|
||||
plan.loop.outerShape.assign(
|
||||
logicalCopyShape->dimensions.begin(),
|
||||
logicalCopyShape->dimensions.end() - contiguousSuffixRank);
|
||||
SmallVector<int64_t> chunkShape(
|
||||
logicalCopyShape->dimensions.end() - contiguousSuffixRank,
|
||||
logicalCopyShape->dimensions.end());
|
||||
auto outerElements = checkedPositiveProduct(plan.loop.outerShape);
|
||||
auto chunkElements = checkedPositiveProduct(chunkShape);
|
||||
auto chunkBytes = failed(chunkElements)
|
||||
? FailureOr<int64_t>(failure())
|
||||
: checkedPositiveMul(*chunkElements, elementByteWidth);
|
||||
if (failed(outerElements) || failed(chunkBytes))
|
||||
plan.loop.outerShape.assign(access->getOuterShape().begin(), access->getOuterShape().end());
|
||||
auto chunkBytes = checkedPositiveMul(access->getRunElements(), elementByteWidth);
|
||||
if (failed(chunkBytes))
|
||||
return failure();
|
||||
plan.loop.outerElements = *outerElements;
|
||||
plan.loop.outerElements = access->getRunCount();
|
||||
plan.loop.chunkBytes = *chunkBytes;
|
||||
auto targetStrides = getOuterByteStrides(
|
||||
target, *logicalCopyShape, plan.loop.outerShape.size());
|
||||
auto sourceStrides = getOuterByteStrides(
|
||||
source, *logicalCopyShape, plan.loop.outerShape.size());
|
||||
if (failed(targetStrides) || failed(sourceStrides))
|
||||
return failure();
|
||||
plan.loop.targetOuterByteStrides = std::move(*targetStrides);
|
||||
plan.loop.sourceOuterByteStrides = std::move(*sourceStrides);
|
||||
if (plan.loop.chunkBytes <= 0)
|
||||
auto scaleStrides = [&](ArrayRef<int64_t> strides, SmallVectorImpl<int64_t> &result) {
|
||||
for (int64_t stride : strides) {
|
||||
auto byteStride = checkedPositiveMul(stride, elementByteWidth);
|
||||
if (failed(byteStride)) return failure();
|
||||
result.push_back(*byteStride);
|
||||
}
|
||||
return success();
|
||||
};
|
||||
if (failed(scaleStrides(access->getTargetOuterStrides(), plan.loop.targetOuterByteStrides))
|
||||
|| failed(scaleStrides(access->getSourceOuterStrides(), plan.loop.sourceOuterByteStrides)))
|
||||
return failure();
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -94,8 +94,12 @@ foldDenseSubview(DenseElementsAttr denseAttr, ArrayRef<int64_t> staticOffsets, A
|
||||
if (numResultElements < 0)
|
||||
return failure();
|
||||
|
||||
auto sourceStrides = computeRowMajorStrides(sourceType.getShape());
|
||||
auto resultStrides = computeRowMajorStrides(resultTensorType.getShape());
|
||||
auto checkedSourceStrides = computeRowMajorStridesChecked(sourceType.getShape());
|
||||
if (failed(checkedSourceStrides)) return failure();
|
||||
auto sourceStrides = std::move(*checkedSourceStrides);
|
||||
auto checkedResultStrides = computeRowMajorStridesChecked(resultTensorType.getShape());
|
||||
if (failed(checkedResultStrides)) return failure();
|
||||
auto resultStrides = std::move(*checkedResultStrides);
|
||||
SmallVector<Attribute> sourceValues(denseAttr.getValues<Attribute>());
|
||||
SmallVector<Attribute> resultValues(numResultElements);
|
||||
for (int64_t i = 0; i < numResultElements; ++i) {
|
||||
|
||||
@@ -218,7 +218,9 @@ static FailureOr<DenseElementsAttr> foldConstantAlloc(memref::AllocOp allocOp, M
|
||||
return failure();
|
||||
|
||||
SmallVector<Attribute> resultValues(numElements, fillValue);
|
||||
auto resultStrides = computeRowMajorStrides(resultTensorType.getShape());
|
||||
auto checkedResultStrides = computeRowMajorStridesChecked(resultTensorType.getShape());
|
||||
if (failed(checkedResultStrides)) return failure();
|
||||
auto resultStrides = std::move(*checkedResultStrides);
|
||||
|
||||
llvm::sort(copies, [](const ConstantSubviewCopy& lhs, const ConstantSubviewCopy& rhs) {
|
||||
return lhs.copyOp->isBeforeInBlock(rhs.copyOp);
|
||||
@@ -232,7 +234,11 @@ static FailureOr<DenseElementsAttr> foldConstantAlloc(memref::AllocOp allocOp, M
|
||||
|| sourceType.getRank() != static_cast<int64_t>(copy.strides.size()))
|
||||
return failure();
|
||||
|
||||
auto sourceStrides = computeRowMajorStrides(sourceType.getShape());
|
||||
auto checkedSourceStrides = computeRowMajorStridesChecked(sourceType.getShape());
|
||||
|
||||
if (failed(checkedSourceStrides)) return failure();
|
||||
|
||||
auto sourceStrides = std::move(*checkedSourceStrides);
|
||||
SmallVector<Attribute> sourceValues(copy.source.getValues<Attribute>());
|
||||
for (auto [linearIndex, value] : llvm::enumerate(sourceValues)) {
|
||||
SmallVector<int64_t> sourceIndices =
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <tuple>
|
||||
|
||||
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
|
||||
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/Passes/Transforms/LocalMemoryPlanning/LocalMemoryPlanning.hpp"
|
||||
#include "src/Accelerators/PIM/Passes/PIMPasses.h"
|
||||
@@ -149,7 +150,7 @@ FailureOr<CoreMemoryPlan> buildCoreMemoryPlan(Operation* coreLikeOp) {
|
||||
plan.intervals = std::move(*intervals);
|
||||
auto placements = planLocalMemoryPlacements(plan.intervals, kPimLocalMemoryAddressLimit);
|
||||
if (failed(placements)) {
|
||||
coreLikeOp->emitError("Pim local-memory plan exceeds the signed int32 address range");
|
||||
pim::emitCandidateResourceError(coreLikeOp, "Pim local-memory plan exceeds the signed int32 address range");
|
||||
return failure();
|
||||
}
|
||||
plan.placements = std::move(*placements);
|
||||
|
||||
@@ -11,6 +11,7 @@ struct PreparedSpatialSchedules {
|
||||
std::vector<MergeScheduleResult> candidates;
|
||||
};
|
||||
|
||||
// Success always includes at least one candidate, including an empty compute graph.
|
||||
mlir::FailureOr<PreparedSpatialSchedules> prepareSpatialSchedules(
|
||||
mlir::ModuleOp module, const SchedulingTarget &target,
|
||||
size_t pipelineStages, bool collectCandidates);
|
||||
|
||||
+53
-34
@@ -15,6 +15,7 @@
|
||||
#include "llvm/ADT/DenseSet.h"
|
||||
#include "llvm/ADT/SetVector.h"
|
||||
#include "llvm/Support/Casting.h"
|
||||
#include "llvm/Support/CheckedArithmetic.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
@@ -30,6 +31,7 @@
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/CopyAccessPlan.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ShapingUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
|
||||
#include "src/Support/TypeUtilities.hpp"
|
||||
@@ -55,6 +57,7 @@ struct SchedulerCostModel {
|
||||
|
||||
const SchedulingTarget& target;
|
||||
ComputeCostKind kind = ComputeCostKind::Execution;
|
||||
mutable bool projectionFallbackReported = false;
|
||||
|
||||
static Cost ceilDiv(Cost numerator, Cost denominator) {
|
||||
assert(denominator > 0 && "denominator must be positive");
|
||||
@@ -534,18 +537,10 @@ static bool isSharedMemoryInput(Value input, const SchedulingTarget &target) {
|
||||
Attribute value;
|
||||
if (!matchPattern(input, m_Constant(&value)) || !isa<ElementsAttr>(value))
|
||||
return false;
|
||||
auto dense = dyn_cast<DenseElementsAttr>(value);
|
||||
if (!target.zeroSplatUsesSharedMemory && dense && dense.isSplat()) {
|
||||
auto element = dense.getSplatValue<Attribute>();
|
||||
if (auto floating = dyn_cast<FloatAttr>(element))
|
||||
return !floating.getValue().isZero() || floating.getValue().isNegative();
|
||||
if (auto integer = dyn_cast<IntegerAttr>(element))
|
||||
return !integer.getValue().isZero();
|
||||
}
|
||||
return true;
|
||||
return target.zeroSplatUsesSharedMemory || !isZeroBitPatternSplat(value);
|
||||
}
|
||||
|
||||
static std::pair<Cost, Cost> getProjectionMemoryRequests(Type type, Value projection,
|
||||
static FailureOr<std::pair<Cost, Cost>> getProjectionMemoryRequests(Type type, Value projection,
|
||||
const SchedulerCostModel &costModel) {
|
||||
Cost bytes = costModel.getByteSize(type, costModel.getComputeBitwidth(type));
|
||||
Cost requests = 1;
|
||||
@@ -553,40 +548,53 @@ static std::pair<Cost, Cost> getProjectionMemoryRequests(Type type, Value projec
|
||||
extract && extract.getSourceType().hasStaticShape()
|
||||
&& !ShapedType::isDynamicShape(extract.getStaticSizes())
|
||||
&& !ShapedType::isDynamicShape(extract.getStaticStrides())) {
|
||||
auto strides = computeRowMajorStrides(extract.getSourceType().getShape());
|
||||
Cost run = 1;
|
||||
for (size_t dim = strides.size(); dim-- > 0;) {
|
||||
Cost size = extract.getStaticSizes()[dim];
|
||||
if (size == 1) continue;
|
||||
if (checkedMultiply(static_cast<Cost>(strides[dim]),
|
||||
static_cast<Cost>(extract.getStaticStrides()[dim])) != run)
|
||||
break;
|
||||
run = checkedMultiply(run, size);
|
||||
auto unavailable = [&]() -> FailureOr<std::pair<Cost, Cost>> {
|
||||
if (!costModel.projectionFallbackReported) {
|
||||
extract.emitWarning("projection memory cost unavailable: using one request per element because static copy geometry exceeds supported offset or stride limits");
|
||||
costModel.projectionFallbackReported = true;
|
||||
}
|
||||
// A physical copy cannot need more than one request per element. Round
|
||||
// each element separately so sub-byte elements cannot undercharge service.
|
||||
return std::make_pair(SchedulerCostModel::ceilDiv(costModel.getComputeBitwidth(type), Cost{8}),
|
||||
static_cast<Cost>(cast<ShapedType>(type).getNumElements()));
|
||||
};
|
||||
auto strides = computeRowMajorStridesChecked(extract.getSourceType().getShape());
|
||||
auto targetStrides = computeRowMajorStridesChecked(extract.getStaticSizes());
|
||||
if (failed(strides) || failed(targetStrides)) return unavailable();
|
||||
for (size_t dim = 0; dim < strides->size(); ++dim) {
|
||||
auto stride = llvm::checkedMul((*strides)[dim], extract.getStaticStrides()[dim]);
|
||||
if (!stride) return unavailable();
|
||||
(*strides)[dim] = *stride;
|
||||
}
|
||||
Cost elements = cast<ShapedType>(type).getNumElements();
|
||||
if (elements) {
|
||||
auto access = CopyAccessPlan::build(extract.getStaticSizes(), *strides, *targetStrides);
|
||||
if (failed(access))
|
||||
return unavailable();
|
||||
Cost run = access->getRunElements();
|
||||
requests = checkedMultiply(requests, elements / run);
|
||||
bytes = SchedulerCostModel::ceilDiv(
|
||||
checkedMultiply(run, costModel.getComputeBitwidth(type)), Cost{8});
|
||||
}
|
||||
}
|
||||
return {bytes, requests};
|
||||
return std::make_pair(bytes, requests);
|
||||
}
|
||||
|
||||
static SharedMemoryDemand getInputMemoryDemand(const InputFragment &fragment,
|
||||
static FailureOr<SharedMemoryDemand> getInputMemoryDemand(const InputFragment &fragment,
|
||||
const SchedulerCostModel &costModel) {
|
||||
auto [bytes, runs] = getProjectionMemoryRequests(fragment.type, fragment.projection, costModel);
|
||||
return costModel.target.getSharedMemoryDemand(bytes,
|
||||
checkedMultiply(runs, fragment.count), false);
|
||||
auto access = getProjectionMemoryRequests(fragment.type, fragment.projection, costModel);
|
||||
if (failed(access)) return failure();
|
||||
return costModel.target.getSharedMemoryDemand(access->first,
|
||||
checkedMultiply(access->second, fragment.count), false);
|
||||
}
|
||||
|
||||
static SharedMemoryDemand getCapturedConstantMemoryDemand(
|
||||
static FailureOr<SharedMemoryDemand> getCapturedConstantMemoryDemand(
|
||||
Operation *owner, const SchedulerCostModel &costModel) {
|
||||
// shortcut: later aliasing/load folding can change baseline requests; exact
|
||||
// accounting requires a placed, bufferized program.
|
||||
SharedMemoryDemand demand;
|
||||
owner->walk([&](Operation *user) {
|
||||
if (user == owner) return;
|
||||
auto result = owner->walk([&](Operation *user) -> WalkResult {
|
||||
if (user == owner) return WalkResult::advance();
|
||||
for (Value value : user->getOperands()) {
|
||||
if (isa<BlockArgument>(value) || !isSharedMemoryInput(value, costModel.target)) continue;
|
||||
if (auto extract = dyn_cast<tensor::ExtractOp>(user)) {
|
||||
@@ -618,9 +626,13 @@ static SharedMemoryDemand getCapturedConstantMemoryDemand(
|
||||
}
|
||||
auto extract = dyn_cast<tensor::ExtractSliceOp>(user);
|
||||
Type type = extract ? extract.getResult().getType() : value.getType();
|
||||
demand += getInputMemoryDemand({type, count, extract ? extract.getResult() : value}, costModel);
|
||||
auto memory = getInputMemoryDemand({type, count, extract ? extract.getResult() : value}, costModel);
|
||||
if (failed(memory)) return WalkResult::interrupt();
|
||||
demand += *memory;
|
||||
}
|
||||
return WalkResult::advance();
|
||||
});
|
||||
if (result.wasInterrupted()) return failure();
|
||||
return demand;
|
||||
}
|
||||
|
||||
@@ -840,10 +852,11 @@ static LogicalResult collectPublicationTransfer(Value input, size_t targetIndex,
|
||||
if (auto fragments = getProjectedInputFragments(input, consumer)) {
|
||||
for (const InputFragment &fragment : *fragments) {
|
||||
Cost bytes = costModel.getByteSize(fragment.type, costModel.getComputeBitwidth(fragment.type));
|
||||
auto [storeBytes, storeRuns] = getProjectionMemoryRequests(
|
||||
auto access = getProjectionMemoryRequests(
|
||||
fragment.type, fragment.projection, costModel);
|
||||
if (failed(access)) return failure();
|
||||
graph.publicationTransfers.push_back({sourceIndex, targetIndex, value,
|
||||
0, fragment.projection, bytes, fragment.count, {}, storeBytes, storeRuns});
|
||||
0, fragment.projection, bytes, fragment.count, {}, access->first, access->second});
|
||||
}
|
||||
} else {
|
||||
Cost bytes = costModel.getByteSize(input.getType(), costModel.getComputeBitwidth(input.getType()));
|
||||
@@ -1292,8 +1305,11 @@ FailureOr<ComputeGraph> buildComputeGraph(Operation* entryOp,
|
||||
DenseMap<Operation *, SharedMemoryDemand> capturedMemory;
|
||||
for (const auto& [targetIndex, node] : llvm::enumerate(graph.nodes)) {
|
||||
auto [captured, inserted] = capturedMemory.try_emplace(node.instance.op);
|
||||
if (inserted)
|
||||
captured->second = getCapturedConstantMemoryDemand(node.instance.op, costModel);
|
||||
if (inserted) {
|
||||
auto memory = getCapturedConstantMemoryDemand(node.instance.op, costModel);
|
||||
if (failed(memory)) return failure();
|
||||
captured->second = *memory;
|
||||
}
|
||||
auto memory = captured->second;
|
||||
memory.readBytes = checkedMultiply(memory.readBytes, static_cast<Cost>(node.instance.laneCount));
|
||||
memory.readRequests = checkedMultiply(memory.readRequests, static_cast<Cost>(node.instance.laneCount));
|
||||
@@ -1304,8 +1320,11 @@ FailureOr<ComputeGraph> buildComputeGraph(Operation* entryOp,
|
||||
size_t publicationsBefore = graph.publicationTransfers.size();
|
||||
if (isSharedMemoryInput(input, target)) {
|
||||
if (auto fragments = getProjectedInputFragments(input, node.instance)) {
|
||||
for (const InputFragment &fragment : *fragments)
|
||||
graph.baselineMemory += getInputMemoryDemand(fragment, costModel);
|
||||
for (const InputFragment &fragment : *fragments) {
|
||||
auto memory = getInputMemoryDemand(fragment, costModel);
|
||||
if (failed(memory)) return failure();
|
||||
graph.baselineMemory += *memory;
|
||||
}
|
||||
} else {
|
||||
graph.baselineMemory += target.getSharedMemoryDemand(
|
||||
costModel.getByteSize(input.getType(), costModel.getComputeBitwidth(input.getType())),
|
||||
|
||||
+30
-8
@@ -6,11 +6,33 @@
|
||||
|
||||
namespace onnx_mlir::spatial {
|
||||
|
||||
bool WholeScheduleCost::eligible() const {
|
||||
bool ScheduleRecurrenceEvidence::proved() const {
|
||||
return status == Proved && periodGenerations && intervalNs
|
||||
&& std::isfinite(*intervalNs) && *intervalNs > 0;
|
||||
}
|
||||
|
||||
std::optional<bool> WholeScheduleCost::convergenceProved() const {
|
||||
if (recurrence[0].status == ScheduleRecurrenceEvidence::NotChecked
|
||||
&& recurrence[1].status == ScheduleRecurrenceEvidence::NotChecked)
|
||||
return std::nullopt;
|
||||
return recurrence[0].proved() && recurrence[1].proved();
|
||||
}
|
||||
|
||||
std::optional<bool> WholeScheduleCost::loweringSupported() const {
|
||||
if (loweringSupport == StaticLoweringSupport::Unknown) return std::nullopt;
|
||||
return loweringSupport == StaticLoweringSupport::Supported;
|
||||
}
|
||||
|
||||
bool WholeScheduleCost::available() const {
|
||||
return primary && alternative && std::isfinite(*primary) && std::isfinite(*alternative)
|
||||
&& *primary > 0 && *alternative > 0 && relativeUncertainty
|
||||
&& std::isfinite(*relativeUncertainty) && *relativeUncertainty >= 0
|
||||
&& *relativeUncertainty < 1;
|
||||
&& *primary > 0 && *alternative > 0;
|
||||
}
|
||||
|
||||
bool WholeScheduleCost::eligible() const {
|
||||
return available() && convergenceProved() == true
|
||||
&& loweringSupport == StaticLoweringSupport::Supported && selectionMargin
|
||||
&& std::isfinite(*selectionMargin) && *selectionMargin >= 0
|
||||
&& *selectionMargin < 1;
|
||||
}
|
||||
|
||||
size_t selectPipelineCandidate(llvm::ArrayRef<WholeScheduleCost> scores, std::string &reason) {
|
||||
@@ -21,17 +43,17 @@ size_t selectPipelineCandidate(llvm::ArrayRef<WholeScheduleCost> scores, std::st
|
||||
size_t best = 0;
|
||||
auto upper = [&](size_t i) {
|
||||
return std::max(*scores[i].primary, *scores[i].alternative)
|
||||
/ (1 - *scores[i].relativeUncertainty);
|
||||
/ (1 - *scores[i].selectionMargin);
|
||||
};
|
||||
for (size_t i = 1; i < scores.size(); ++i)
|
||||
if (scores[i].eligible() && upper(i) < upper(best)) best = i;
|
||||
if (!best) { reason = "original has the lowest conservative estimate"; return 0; }
|
||||
double originalLower = std::min(*scores.front().primary, *scores.front().alternative);
|
||||
if (upper(best) >= originalLower / (1 + *scores.front().relativeUncertainty)) {
|
||||
reason = "candidate and original overlap static prediction uncertainty";
|
||||
if (upper(best) >= originalLower / (1 + *scores.front().selectionMargin)) {
|
||||
reason = "candidate and original overlap the selection policy margin";
|
||||
return 0;
|
||||
}
|
||||
reason = "candidate improves on original under both arbitration tie policies and prediction uncertainty";
|
||||
reason = "candidate improves on original under both arbitration tie policies and the selection margin";
|
||||
return best;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -8,6 +8,7 @@
|
||||
namespace onnx_mlir::spatial {
|
||||
|
||||
struct MergeScheduleResult;
|
||||
inline constexpr double kPipelineSelectionMargin = 0.021;
|
||||
|
||||
size_t selectPipelineCandidate(llvm::ArrayRef<WholeScheduleCost> scores,
|
||||
std::string &reason);
|
||||
|
||||
+57
-48
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -39,20 +40,36 @@ struct ScheduledComputeMaterializationResult;
|
||||
struct DeferredTransferPlan;
|
||||
struct DeferredBoundaryPlan;
|
||||
|
||||
// Unknown lacks a proof. Unsupported identifies a known static backend violation.
|
||||
// Supported covers checked static forms, not final allocation or code generation.
|
||||
enum class StaticLoweringSupport { Unknown, Supported, Unsupported };
|
||||
|
||||
struct ScheduleRecurrenceEvidence {
|
||||
enum Status { NotChecked, Unproved, Proved };
|
||||
Status status = NotChecked;
|
||||
size_t periodGenerations = 0;
|
||||
std::optional<double> intervalNs;
|
||||
std::string reason;
|
||||
bool proved() const;
|
||||
};
|
||||
|
||||
struct WholeScheduleCost {
|
||||
// Steady-state intervals in nanoseconds. The alternative tests sensitivity.
|
||||
std::optional<double> primary, alternative;
|
||||
std::string reason;
|
||||
std::string identity;
|
||||
std::optional<double> relativeUncertainty;
|
||||
// Selection policy only. This is not a prediction error bound.
|
||||
std::optional<double> selectionMargin;
|
||||
std::array<ScheduleRecurrenceEvidence, 2> recurrence;
|
||||
StaticLoweringSupport loweringSupport = StaticLoweringSupport::Unknown;
|
||||
struct AnalysisTiming {
|
||||
double extractionSeconds = 0, expansionSeconds = 0, evaluationSeconds = 0;
|
||||
} timing;
|
||||
std::optional<bool> convergenceProved() const;
|
||||
std::optional<bool> loweringSupported() const;
|
||||
bool available() const;
|
||||
bool eligible() const;
|
||||
};
|
||||
|
||||
struct ScheduleCostQuery {
|
||||
std::string identity;
|
||||
std::function<WholeScheduleCost(llvm::json::Object &)> evaluate;
|
||||
};
|
||||
|
||||
struct SchedulingTarget {
|
||||
size_t processorCount = 0;
|
||||
size_t residentWeightCapacity = 0;
|
||||
@@ -76,38 +93,38 @@ struct SchedulingTarget {
|
||||
Cost sharedMemoryWriteLatencyNs = 0;
|
||||
bool zeroSplatUsesSharedMemory = true;
|
||||
|
||||
// Input is a verified, target-lowered candidate. Preparation and evaluation
|
||||
// are read-only. An absent query keeps the original schedule.
|
||||
std::function<mlir::FailureOr<ScheduleCostQuery>(mlir::ModuleOp,
|
||||
llvm::json::Object &)> prepareScheduleCost;
|
||||
// Optional read-only estimate over realized Spatial. Finite costs do not
|
||||
// establish convergence or lowering support. Callers must validate these
|
||||
// properties and apply the Spatial selection policy.
|
||||
std::function<WholeScheduleCost(mlir::ModuleOp,
|
||||
llvm::json::Object &)> estimateRealizedScheduleCost;
|
||||
std::string wholeScheduleCostUnavailable;
|
||||
std::function<void(const ComputeGraph &,
|
||||
const ScheduledComputeMaterializationResult &, const DeferredTransferPlan &,
|
||||
const DeferredBoundaryPlan &)> reportPeriodicSchedule;
|
||||
|
||||
Cost getLocalMemoryServiceNs(Cost bytes, bool write) const {
|
||||
Cost accesses = bytes / localMemoryWidthBytes
|
||||
+ (bytes % localMemoryWidthBytes != 0);
|
||||
return checkedMultiply(accesses, checkedMultiply(localMemoryPeriodNs,
|
||||
write ? localMemoryWriteLatencyCycles : localMemoryReadLatencyCycles));
|
||||
template <typename Arithmetic = SchedulingCostArithmetic>
|
||||
Cost getLocalMemoryServiceNs(Cost bytes, bool write, Arithmetic math = {}) const {
|
||||
return math.multiply(math.divideUp(bytes, localMemoryWidthBytes),
|
||||
math.multiply(localMemoryPeriodNs,
|
||||
write ? localMemoryWriteLatencyCycles : localMemoryReadLatencyCycles));
|
||||
}
|
||||
|
||||
template <typename Arithmetic = SchedulingCostArithmetic>
|
||||
SharedMemoryDemand getSharedMemoryDemand(
|
||||
Cost bytesPerRequest, Cost requests, bool write) const {
|
||||
Cost bytesPerRequest, Cost requests, bool write, Arithmetic math = {}) const {
|
||||
SharedMemoryDemand demand;
|
||||
if (write) {
|
||||
demand.writeBytes = checkedMultiply(bytesPerRequest, requests);
|
||||
demand.writeBytes = math.multiply(bytesPerRequest, requests);
|
||||
demand.writeRequests = requests;
|
||||
} else {
|
||||
demand.readBytes = checkedMultiply(bytesPerRequest, requests);
|
||||
demand.readBytes = math.multiply(bytesPerRequest, requests);
|
||||
demand.readRequests = requests;
|
||||
}
|
||||
if (sharedMemoryWidthBytes) {
|
||||
Cost accesses = bytesPerRequest / sharedMemoryWidthBytes
|
||||
+ (bytesPerRequest % sharedMemoryWidthBytes != 0);
|
||||
demand.serviceNs = checkedMultiply(requests, checkedMultiply(accesses,
|
||||
if (sharedMemoryWidthBytes)
|
||||
demand.serviceNs = math.multiply(requests, math.multiply(
|
||||
math.divideUp(bytesPerRequest, sharedMemoryWidthBytes),
|
||||
write ? sharedMemoryWriteLatencyNs : sharedMemoryReadLatencyNs));
|
||||
}
|
||||
return demand;
|
||||
}
|
||||
|
||||
@@ -127,33 +144,25 @@ struct SchedulingTarget {
|
||||
Cost matrixInputBufferLatencyCycles = 0;
|
||||
bool matrixPipeline = true;
|
||||
|
||||
Cost getMatrixComputeLatencyNs(Cost inputBitwidth) const {
|
||||
auto divideRoundedUp = [](Cost value, Cost width) {
|
||||
return value / width + (value % width != 0);
|
||||
};
|
||||
Cost inputTimes = divideRoundedUp(inputBitwidth, matrixInputResolutionBits);
|
||||
Cost inputPasses = divideRoundedUp(matrixRows, matrixInputParallelism);
|
||||
Cost outputPasses = divideRoundedUp(matrixColumns, matrixOutputParallelism);
|
||||
Cost readCycles = divideRoundedUp(matrixReadLatencyNs, matrixPeriodNs);
|
||||
Cost frontStage = matrixInputBufferLatencyCycles + matrixInputLatencyCycles
|
||||
+ readCycles + matrixSampleLatencyCycles;
|
||||
template <typename Arithmetic = SchedulingCostArithmetic>
|
||||
Cost getMatrixComputeLatencyNs(Cost inputBitwidth, Arithmetic math = {}) const {
|
||||
Cost inputTimes = math.divideUp(inputBitwidth, matrixInputResolutionBits);
|
||||
Cost inputPasses = math.divideUp(matrixRows, matrixInputParallelism);
|
||||
Cost outputPasses = math.divideUp(matrixColumns, matrixOutputParallelism);
|
||||
Cost readCycles = math.divideUp(matrixReadLatencyNs, matrixPeriodNs);
|
||||
Cost frontStage = math.add(math.add(matrixInputBufferLatencyCycles,
|
||||
matrixInputLatencyCycles), math.add(readCycles, matrixSampleLatencyCycles));
|
||||
Cost backPipe = std::max(matrixOutputLatencyCycles,
|
||||
checkedAdd(matrixShiftLatencyCycles,
|
||||
matrixBufferLatencyCycles));
|
||||
Cost backStage = checkedAdd(
|
||||
checkedAdd(matrixOutputLatencyCycles, matrixShiftLatencyCycles),
|
||||
matrixBufferLatencyCycles);
|
||||
backStage = checkedAdd(backStage, checkedMultiply(outputPasses - 1, backPipe));
|
||||
Cost totalTimes = checkedMultiply(inputTimes, inputPasses);
|
||||
math.add(matrixShiftLatencyCycles, matrixBufferLatencyCycles));
|
||||
Cost backStage = math.add(math.add(matrixOutputLatencyCycles,
|
||||
matrixShiftLatencyCycles), matrixBufferLatencyCycles);
|
||||
backStage = math.add(backStage, math.multiply(math.subtract(outputPasses, 1), backPipe));
|
||||
Cost totalTimes = math.multiply(inputTimes, inputPasses);
|
||||
if (!matrixPipeline)
|
||||
return checkedMultiply(
|
||||
checkedMultiply(checkedAdd(frontStage, backStage), totalTimes),
|
||||
matrixPeriodNs);
|
||||
return math.multiply(math.multiply(math.add(frontStage, backStage), totalTimes), matrixPeriodNs);
|
||||
Cost stagePipe = std::max(frontStage, backStage);
|
||||
return checkedMultiply(
|
||||
checkedAdd(checkedAdd(frontStage, backStage),
|
||||
checkedMultiply(totalTimes - 1, stagePipe)),
|
||||
matrixPeriodNs);
|
||||
return math.multiply(math.add(math.add(frontStage, backStage),
|
||||
math.multiply(math.subtract(totalTimes, 1), stagePipe)), matrixPeriodNs);
|
||||
}
|
||||
|
||||
Cost getInterProcessorLatencyNs(size_t source,
|
||||
|
||||
@@ -36,6 +36,13 @@ inline T checkedMultiply(T lhs, T rhs) {
|
||||
return lhs * rhs;
|
||||
}
|
||||
|
||||
struct SchedulingCostArithmetic {
|
||||
Cost add(Cost a, Cost b) const { return checkedAdd(a, b); }
|
||||
Cost multiply(Cost a, Cost b) const { return checkedMultiply(a, b); }
|
||||
Cost subtract(Cost a, Cost b) const { assert(a >= b); return a - b; }
|
||||
Cost divideUp(Cost a, Cost b) const { assert(b); return a / b + (a % b != 0); }
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline T addOrMax(T lhs, T rhs) {
|
||||
static_assert(std::is_unsigned_v<T>, "addOrMax only supports unsigned types");
|
||||
|
||||
@@ -30,6 +30,31 @@ add_pim_unittest(PimMemoryLivenessPlannerTest
|
||||
OMPimCompilerUtils
|
||||
)
|
||||
|
||||
add_pim_unittest(CopyAccessPlanTest
|
||||
CopyAccessPlanTest.cpp
|
||||
LINK_LIBS PRIVATE OMPimCompilerUtils
|
||||
)
|
||||
add_test(NAME CopyAccessPlanChecksActive COMMAND CopyAccessPlanTest --check-failure)
|
||||
set_tests_properties(CopyAccessPlanChecksActive PROPERTIES WILL_FAIL TRUE LABELS pim-unittest)
|
||||
add_test(NAME RowMajorStrideOverflow
|
||||
COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_ShapeUtils.py"
|
||||
--test "$<TARGET_FILE:CopyAccessPlanTest>")
|
||||
set_tests_properties(RowMajorStrideOverflow PROPERTIES LABELS pim-unittest)
|
||||
|
||||
add_pim_unittest(ComputeInputMaterializationTest
|
||||
ComputeInputMaterializationTest.cpp
|
||||
LINK_LIBS PRIVATE OMPimCompilerUtils
|
||||
)
|
||||
|
||||
add_pim_unittest(ProjectionMemoryCostTest
|
||||
ProjectionMemoryCostTest.cpp
|
||||
LINK_LIBS PRIVATE OMPimCompilerUtils
|
||||
)
|
||||
add_pim_unittest(PimAbstractExecutionTest
|
||||
PimAbstractExecutionTest.cpp
|
||||
LINK_LIBS PRIVATE OMPimCompilerUtils
|
||||
)
|
||||
|
||||
add_pim_unittest(SpatialSchedulingTargetTest
|
||||
SpatialSchedulingTargetTest.cpp
|
||||
|
||||
@@ -77,3 +102,8 @@ add_pim_unittest(PimPipelineSelectionTest
|
||||
PimPipelineSelectionTest.cpp
|
||||
LINK_LIBS PRIVATE OMPimCompilerUtils
|
||||
)
|
||||
|
||||
add_test(NAME PimSpatialSelectionTest
|
||||
COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_PimSpatialSelection.py"
|
||||
--compiler "$<TARGET_FILE:onnx-mlir>")
|
||||
set_tests_properties(PimSpatialSelectionTest PROPERTIES LABELS pim-unittest)
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#include "TestCheck.hpp"
|
||||
#include <cmath>
|
||||
#include "mlir/Dialect/Arith/IR/Arith.h"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
#include "mlir/Dialect/SCF/IR/SCF.h"
|
||||
#include "mlir/Dialect/Tensor/IR/Tensor.h"
|
||||
#include "mlir/IR/BuiltinOps.h"
|
||||
#include "mlir/IR/Verifier.h"
|
||||
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/ComputeLikeRegionUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Conversion/SpatialToPim/Common.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
|
||||
|
||||
using namespace mlir;
|
||||
using namespace onnx_mlir;
|
||||
using namespace onnx_mlir::spatial;
|
||||
|
||||
static void testZeroContent(OpBuilder &builder) {
|
||||
auto type = RankedTensorType::get({4}, builder.getF32Type());
|
||||
for (double value : {0.0, -0.0, 1.0, -1.0}) {
|
||||
auto attribute = DenseElementsAttr::get(type, builder.getF32FloatAttr(value));
|
||||
TEST_CHECK(isZeroBitPatternSplat(attribute) == (value == 0.0 && !std::signbit(value)));
|
||||
}
|
||||
auto integerType = RankedTensorType::get({4}, builder.getI32Type());
|
||||
TEST_CHECK(isZeroBitPatternSplat(DenseElementsAttr::get(integerType, builder.getI32IntegerAttr(0))));
|
||||
TEST_CHECK(!isZeroBitPatternSplat(DenseElementsAttr::get(integerType, builder.getI32IntegerAttr(1))));
|
||||
TEST_CHECK(!isZeroBitPatternSplat(DenseElementsAttr::get(type, ArrayRef<float>{0, 1, 0, 0})));
|
||||
TEST_CHECK(!isZeroBitPatternSplat(builder.getF32FloatAttr(0)));
|
||||
TEST_CHECK(!isZeroBitPatternSplat({}));
|
||||
}
|
||||
|
||||
static void testInputPlan(MLIRContext &context, bool batched) {
|
||||
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module->getBodyRegion());
|
||||
auto loc = module->getLoc();
|
||||
auto type = RankedTensorType::get({1, 4}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({type}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
Value constant = arith::ConstantOp::create(builder, loc,
|
||||
DenseElementsAttr::get(type, builder.getF32FloatAttr(2)));
|
||||
Value view = tensor::CastOp::create(builder, loc, type, constant);
|
||||
SmallVector<Value> inputs{constant, function.getArgument(0), constant, view, function.getArgument(0)};
|
||||
Operation *owner;
|
||||
if (batched) {
|
||||
auto batch = SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
|
||||
builder.getI32IntegerAttr(2), ValueRange{}, inputs);
|
||||
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({0, 1}));
|
||||
owner = batch;
|
||||
} else {
|
||||
auto single = SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, inputs);
|
||||
single->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
owner = single;
|
||||
}
|
||||
auto *body = new Block;
|
||||
owner->getRegion(0).push_back(body);
|
||||
if (batched) body->addArgument(builder.getIndexType(), loc);
|
||||
for (Value input : inputs) body->addArgument(input.getType(), loc);
|
||||
builder.setInsertionPointToStart(body);
|
||||
unsigned first = batched ? 1 : 0;
|
||||
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
|
||||
Value limit = arith::ConstantIndexOp::create(builder, loc, 1000000000000);
|
||||
Value one = arith::ConstantIndexOp::create(builder, loc, 1);
|
||||
auto loop = scf::ForOp::create(builder, loc, zero, limit, one);
|
||||
builder.setInsertionPointToStart(loop.getBody());
|
||||
SpatVAddOp::create(builder, loc, type, body->getArgument(first), body->getArgument(first + 1));
|
||||
SpatVAddOp::create(builder, loc, type, body->getArgument(first + 3), body->getArgument(first));
|
||||
builder.setInsertionPointAfter(loop);
|
||||
auto lastUse = SpatVAddOp::create(builder, loc, type, body->getArgument(first + 2), body->getArgument(first));
|
||||
auto lastFunctionUse = SpatVAddOp::create(builder, loc, type,
|
||||
body->getArgument(first + 4), body->getArgument(first));
|
||||
SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(owner);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(*module)));
|
||||
// Body lowering can remove an input's final use before input materialization.
|
||||
lastUse.erase();
|
||||
lastFunctionUse.erase();
|
||||
|
||||
std::string before;
|
||||
llvm::raw_string_ostream stream(before);
|
||||
module->print(stream);
|
||||
for (unsigned repeat = 0; repeat < 2; ++repeat) {
|
||||
auto constantPlan = planComputeInputMaterialization(owner, 0);
|
||||
auto runtimePlan = planComputeInputMaterialization(owner, 1);
|
||||
auto unusedPlan = planComputeInputMaterialization(owner, 2);
|
||||
auto viewPlan = planComputeInputMaterialization(owner, 3);
|
||||
TEST_CHECK(succeeded(constantPlan) && succeeded(runtimePlan) && succeeded(unusedPlan));
|
||||
TEST_CHECK(*constantPlan == (batched ? ComputeInputMaterialization::CopyAtEntry
|
||||
: ComputeInputMaterialization::ForwardConstant));
|
||||
TEST_CHECK(*runtimePlan == ComputeInputMaterialization::ForwardFunctionInput);
|
||||
auto unusedFunctionPlan = planComputeInputMaterialization(owner, 4);
|
||||
TEST_CHECK(succeeded(unusedFunctionPlan)
|
||||
&& *unusedFunctionPlan == (batched ? ComputeInputMaterialization::ForwardFunctionInput
|
||||
: ComputeInputMaterialization::Unused));
|
||||
TEST_CHECK(*unusedPlan == (batched ? ComputeInputMaterialization::CopyAtEntry
|
||||
: ComputeInputMaterialization::Unused));
|
||||
TEST_CHECK(succeeded(viewPlan) && *viewPlan == ComputeInputMaterialization::CopyAtEntry);
|
||||
}
|
||||
TEST_CHECK(failed(planComputeInputMaterialization(owner, 5)));
|
||||
TEST_CHECK(failed(planComputeInputMaterialization(function, 0)));
|
||||
std::string after;
|
||||
llvm::raw_string_ostream afterStream(after);
|
||||
module->print(afterStream);
|
||||
TEST_CHECK(before == after);
|
||||
}
|
||||
|
||||
static void testOutputReuse(MLIRContext &context) {
|
||||
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
IRRewriter builder(&context);
|
||||
builder.setInsertionPointToStart(module->getBody());
|
||||
auto loc = module->getLoc();
|
||||
auto type = RankedTensorType::get({1, 4}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "reuse", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
Value input = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
|
||||
Value constant = arith::ConstantOp::create(builder, loc,
|
||||
DenseElementsAttr::get(type, builder.getF32FloatAttr(2)));
|
||||
auto first = SpatVAddOp::create(builder, loc, type, input, constant);
|
||||
TEST_CHECK(getReusableOutputTensor(first) == input);
|
||||
auto second = SpatVAddOp::create(builder, loc, type, input, constant);
|
||||
TEST_CHECK(!getReusableOutputTensor(first));
|
||||
TEST_CHECK(getReusableOutputTensor(second) == input);
|
||||
second->moveBefore(first);
|
||||
TEST_CHECK(getReusableOutputTensor(first) == input);
|
||||
TEST_CHECK(!getReusableOutputTensor(second));
|
||||
second->moveAfter(first);
|
||||
TEST_CHECK(!getReusableOutputTensor(first));
|
||||
TEST_CHECK(getReusableOutputTensor(second) == input);
|
||||
Value view = tensor::CastOp::create(builder, loc, type, input);
|
||||
auto fromView = SpatVAddOp::create(builder, loc, type, view, constant);
|
||||
TEST_CHECK(!getReusableOutputTensor(fromView));
|
||||
TEST_CHECK(getBestOutputTensorFromOperandsOrAllocate(builder, fromView)
|
||||
.getDefiningOp<tensor::EmptyOp>());
|
||||
builder.setInsertionPointAfter(fromView);
|
||||
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
|
||||
Value limit = arith::ConstantIndexOp::create(builder, loc, 10);
|
||||
Value one = arith::ConstantIndexOp::create(builder, loc, 1);
|
||||
auto loop = scf::ForOp::create(builder, loc, zero, limit, one, ValueRange{input});
|
||||
builder.setInsertionPointToStart(loop.getBody());
|
||||
auto carried = SpatVAddOp::create(builder, loc, type, loop.getRegionIterArg(0), constant);
|
||||
scf::YieldOp::create(builder, loc, ValueRange{carried});
|
||||
TEST_CHECK(getReusableOutputTensor(carried) == loop.getRegionIterArg(0));
|
||||
TEST_CHECK(getBestOutputTensorFromOperandsOrAllocate(builder, carried) == loop.getRegionIterArg(0));
|
||||
builder.setInsertionPointAfter(loop);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(*module)));
|
||||
}
|
||||
|
||||
int main() {
|
||||
MLIRContext context(MLIRContext::Threading::DISABLED);
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, scf::SCFDialect,
|
||||
tensor::TensorDialect, SpatialDialect>();
|
||||
OpBuilder builder(&context);
|
||||
testZeroContent(builder);
|
||||
testInputPlan(context, false);
|
||||
testInputPlan(context, true);
|
||||
testOutputReuse(context);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "src/Accelerators/PIM/Common/IR/CopyAccessPlan.hpp"
|
||||
#include "src/Accelerators/PIM/Common/IR/ShapeUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/SchedulingTarget.hpp"
|
||||
#include "TestCheck.hpp"
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
using namespace onnx_mlir;
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1 && std::string(argv[1]) == "--stride-overflow") {
|
||||
computeRowMajorStrides({2, std::numeric_limits<int64_t>::max(), 2});
|
||||
return 0;
|
||||
}
|
||||
if (argc > 1) TEST_CHECK(false);
|
||||
auto dense = CopyAccessPlan::build({3, 4}, {4, 1}, {4, 1});
|
||||
TEST_CHECK(mlir::succeeded(dense) && dense->getRunCount() == 1 && dense->getRunElements() == 12);
|
||||
auto strided = CopyAccessPlan::build({3, 4}, {8, 1}, {4, 1});
|
||||
TEST_CHECK(mlir::succeeded(strided) && strided->getRunCount() == 3 && strided->getRunElements() == 4);
|
||||
TEST_CHECK(*strided->getRunOffsets(2) == std::make_pair(int64_t{16}, int64_t{8}));
|
||||
TEST_CHECK(mlir::failed(strided->getRunOffsets(-1)) && mlir::failed(strided->getRunOffsets(3)));
|
||||
auto large = CopyAccessPlan::build({1000000000, 16}, {32, 1}, {16, 1});
|
||||
TEST_CHECK(mlir::succeeded(large) && large->getOuterShape().size() == 1);
|
||||
TEST_CHECK(large->getRunCount() == 1000000000 && large->getRunElements() == 16);
|
||||
TEST_CHECK(*large->getRunOffsets(999999999) == std::make_pair(int64_t{31999999968}, int64_t{15999999984}));
|
||||
auto scalar = CopyAccessPlan::build({}, {}, {});
|
||||
TEST_CHECK(mlir::succeeded(scalar) && scalar->getRunCount() == 1 && scalar->getRunElements() == 1);
|
||||
// Observed input planes and channel-wise publication slices, without PIM IR.
|
||||
auto input = CopyAccessPlan::build({3, 1, 224}, {50176, 224, 1}, {224, 224, 1});
|
||||
TEST_CHECK(mlir::succeeded(input) && input->getRunCount() == 3 && input->getRunElements() == 224);
|
||||
TEST_CHECK(*input->getRunOffsets(1) == std::make_pair(int64_t{50176}, int64_t{224}));
|
||||
for (int64_t channels : {32, 96}) {
|
||||
auto publication = CopyAccessPlan::build({1, channels, 1, 80},
|
||||
{channels * 6400, 6400, 80, 1}, {channels * 80, 80, 80, 1});
|
||||
TEST_CHECK(mlir::succeeded(publication) && publication->getRunCount() == channels);
|
||||
TEST_CHECK(publication->getRunElements() == 80);
|
||||
TEST_CHECK(*publication->getRunOffsets(1) == std::make_pair(int64_t{6400}, int64_t{80}));
|
||||
}
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({2}, {}, {1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({0}, {1}, {1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({-1}, {1}, {1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({2}, {-1}, {1})));
|
||||
int64_t limit = std::numeric_limits<int64_t>::max();
|
||||
TEST_CHECK(mlir::failed(computeRowMajorStridesChecked({2, limit, 2})));
|
||||
auto limitStrides = computeRowMajorStridesChecked({2, limit});
|
||||
TEST_CHECK(mlir::succeeded(limitStrides) && (*limitStrides)[0] == limit);
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({limit, 2}, {2, 1}, {2, 1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({limit, 2}, {3, 1}, {3, 1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({3}, {limit}, {1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({3}, {1}, {limit})));
|
||||
// A run start can fit even when its final element does not.
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({2, 2}, {limit, 1}, {2, 1})));
|
||||
TEST_CHECK(mlir::failed(CopyAccessPlan::build({2, 2}, {2, 1}, {limit, 1})));
|
||||
auto boundary = CopyAccessPlan::build({2}, {limit}, {1});
|
||||
TEST_CHECK(mlir::succeeded(boundary) && boundary->getRunOffsets(1)->first == limit);
|
||||
auto unit = CopyAccessPlan::build({1, 2}, {limit, 1}, {-1, 1});
|
||||
TEST_CHECK(mlir::succeeded(unit) && unit->getRunElements() == 2);
|
||||
|
||||
// Independent element-by-element reference checks ordered offsets, not totals.
|
||||
std::mt19937 random(123);
|
||||
for (unsigned trial = 0; trial < 2000; ++trial) {
|
||||
size_t rank = random() % 4 + 1;
|
||||
llvm::SmallVector<int64_t> shape(rank), source(rank), target(rank);
|
||||
for (auto &size : shape) size = random() % 4 + 1;
|
||||
auto rowMajor = computeRowMajorStrides(shape);
|
||||
for (size_t dim = 0; dim < rank; ++dim) {
|
||||
source[dim] = random() % 2 ? rowMajor[dim] : random() % 20;
|
||||
target[dim] = random() % 2 ? rowMajor[dim] : random() % 20;
|
||||
}
|
||||
auto plan = CopyAccessPlan::build(shape, source, target);
|
||||
TEST_CHECK(mlir::succeeded(plan));
|
||||
int64_t linear = 0;
|
||||
for (int64_t run = 0; run < plan->getRunCount(); ++run) {
|
||||
auto offsets = plan->getRunOffsets(run);
|
||||
TEST_CHECK(mlir::succeeded(offsets));
|
||||
for (int64_t element = 0; element < plan->getRunElements(); ++element, ++linear) {
|
||||
auto indices = delinearizeIndex(linear, shape, rowMajor);
|
||||
TEST_CHECK(offsets->first + element == linearizeIndex(indices, source));
|
||||
TEST_CHECK(offsets->second + element == linearizeIndex(indices, target));
|
||||
}
|
||||
}
|
||||
TEST_CHECK(linear == getNumElements(shape));
|
||||
}
|
||||
|
||||
// The cost caller and lowering use the same geometry; target timing stays separate.
|
||||
for (int64_t width : {8, 16}) {
|
||||
spatial::SchedulingTarget target;
|
||||
target.sharedMemoryWidthBytes = width;
|
||||
target.sharedMemoryReadLatencyNs = width == 8 ? 3 : 8;
|
||||
auto access = CopyAccessPlan::build({3, 5}, {8, 1}, {5, 1});
|
||||
TEST_CHECK(mlir::succeeded(access));
|
||||
auto demand = target.getSharedMemoryDemand(access->getRunElements() * 4, access->getRunCount(), false);
|
||||
TEST_CHECK(demand.readBytes == 60 && demand.readRequests == 3);
|
||||
TEST_CHECK(demand.serviceNs == 3 * ((20 + width - 1) / width) * target.sharedMemoryReadLatencyNs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,864 @@
|
||||
#include "TestCheck.hpp"
|
||||
#include <limits>
|
||||
#include "src/Accelerators/PIM/Compiler/PimPeriodicAnalysis.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimInstructionTiming.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimSpatialCost.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimBinaryFormat.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimDependencyTokens.hpp"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
#include "mlir/Dialect/SCF/IR/SCF.h"
|
||||
#include "mlir/Dialect/Tensor/IR/Tensor.h"
|
||||
#include "mlir/Dialect/Linalg/IR/Linalg.h"
|
||||
#include "mlir/IR/Verifier.h"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
|
||||
|
||||
using namespace onnx_mlir;
|
||||
using namespace mlir;
|
||||
using E = PimExecutionEvent;
|
||||
using O = pim_binary::Opcode;
|
||||
|
||||
static PimInstructionTiming hardware(int64_t period) {
|
||||
PimInstructionTiming target;
|
||||
target.processorCount = 2;
|
||||
target.processorPeriodNs = period;
|
||||
target.localMemoryPeriodNs = 3;
|
||||
target.peerPermissionThenSourceRead = true;
|
||||
target.signalIssueNs = period;
|
||||
target.controlMessageBytes = 1;
|
||||
target.sharedMemoryWidthBytes = 64;
|
||||
target.sharedMemoryReadLatencyNs = 80;
|
||||
target.sharedMemoryWriteLatencyNs = 120;
|
||||
target.sharedMemoryRequestLatencyNs = {2, 3};
|
||||
target.sharedMemoryResponseLatencyNs = {2, 3};
|
||||
target.interProcessorLatencyNs = {0, 2, 2, 0};
|
||||
target.synchronizationRegisterCount = 4;
|
||||
return target;
|
||||
}
|
||||
|
||||
static std::string binary(const std::vector<pim_binary::InstructionRecord> &records) {
|
||||
std::string result(12, '\0');
|
||||
result.replace(0, 4, "PIMB");
|
||||
llvm::support::endian::write32le(result.data() + 4, 1);
|
||||
llvm::support::endian::write32le(result.data() + 8, records.size());
|
||||
for (const auto &record : records) {
|
||||
auto encoded = pim_binary::encodeInstructionRecord(record);
|
||||
result.append(encoded.data(), encoded.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void testTypedResult(PimInstructionTiming target) {
|
||||
PimPeriodicOptions options;
|
||||
options.verifyPeriodicState = true;
|
||||
PimExecutionProgram program{{{E::Work, 29}, {E::Load, 65}}};
|
||||
auto estimate = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(estimate.cost.available() && estimate.cost.convergenceProved() == true);
|
||||
TEST_CHECK(estimate.report.getString("status") == "estimated");
|
||||
TEST_CHECK(estimate.cost.loweringSupport == spatial::StaticLoweringSupport::Unknown);
|
||||
TEST_CHECK(!estimate.cost.eligible() && !estimate.cost.selectionMargin);
|
||||
auto *queue = estimate.report.getObject("memory_controller");
|
||||
for (size_t policy = 0; policy < 2; ++policy) {
|
||||
auto *state = queue->getObject(policy ? "alternative_periodic_state" : "periodic_state");
|
||||
const auto &proof = estimate.cost.recurrence[policy];
|
||||
TEST_CHECK(proof.proved() && state->getString("status") == "proved");
|
||||
TEST_CHECK(proof.intervalNs == state->getNumber("interval_ns"));
|
||||
TEST_CHECK(proof.periodGenerations == state->getInteger("period_generations"));
|
||||
}
|
||||
auto primary = estimate.cost.primary, alternative = estimate.cost.alternative;
|
||||
estimate.report = llvm::json::Object{{"status", "unavailable"}, {"convergence_proved", false}};
|
||||
TEST_CHECK(estimate.cost.primary == primary && estimate.cost.alternative == alternative);
|
||||
TEST_CHECK(estimate.cost.convergenceProved() == true);
|
||||
options.verifyPeriodicState = false;
|
||||
estimate = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(estimate.cost.available() && !estimate.cost.convergenceProved().has_value());
|
||||
TEST_CHECK(estimate.report.getString("status") == "estimated");
|
||||
TEST_CHECK(!estimate.report.getObject("memory_controller")->getObject("periodic_state"));
|
||||
options.intervalWindowEventBudget = 0;
|
||||
for (bool detailed : {false, true}) {
|
||||
options.detailed = detailed;
|
||||
estimate = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(estimate.queue && !estimate.cost.available());
|
||||
TEST_CHECK(!estimate.cost.convergenceProved().has_value());
|
||||
TEST_CHECK(estimate.report.getString("status") == "unavailable");
|
||||
TEST_CHECK(estimate.report.getString("reason") == estimate.cost.reason);
|
||||
TEST_CHECK(estimate.report.getObject("memory_controller")->getString("status") == "unavailable");
|
||||
TEST_CHECK(!estimate.report.getObject("memory_controller")->getObject("periodic_state"));
|
||||
}
|
||||
options.intervalWindowEventBudget.reset();
|
||||
options.detailed = false;
|
||||
options.verifyPeriodicState = true;
|
||||
estimate = evaluatePimExecution({{{E::Wait, 0, 0, 0, 1}, {E::Load, 65}}}, target, options);
|
||||
TEST_CHECK(!estimate.cost.available() && estimate.cost.convergenceProved() == false);
|
||||
TEST_CHECK(!estimate.cost.reason.empty() && !estimate.cost.eligible());
|
||||
target.sharedMemoryWidthBytes = 0;
|
||||
estimate = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(!estimate.cost.available() && !estimate.cost.reason.empty());
|
||||
TEST_CHECK(estimate.report.getString("status") == "unavailable");
|
||||
TEST_CHECK(estimate.report.getString("reason") == estimate.cost.reason);
|
||||
}
|
||||
|
||||
static void testLateRecurrenceAuthority(PimInstructionTiming target) {
|
||||
PimPeriodicOptions options;
|
||||
options.verifyPeriodicState = true;
|
||||
options.intervalWindowEventBudget = 1;
|
||||
PimExecutionProgram program{{{E::Work, 29}, {E::Load, 65}}};
|
||||
auto late = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(late.queue && late.cost.available() && late.cost.convergenceProved() == true);
|
||||
for (const auto &policy : late.queue->policies) {
|
||||
TEST_CHECK(policy.legacy.termination == PimQueueTermination::EventBudget);
|
||||
TEST_CHECK(!policy.legacy.intervalNs && policy.recurrence.proved());
|
||||
TEST_CHECK(policy.proofProcessedEvents > policy.legacy.progress.processedEvents);
|
||||
TEST_CHECK(policy.intervalNs() == policy.recurrence.intervalNs);
|
||||
TEST_CHECK(policy.termination == PimQueueTermination::RecurrenceProved);
|
||||
}
|
||||
auto first = serializePimQueueEvaluation(*late.queue);
|
||||
auto second = serializePimQueueEvaluation(*late.queue);
|
||||
TEST_CHECK(first.getString("status") == "estimated" && !first.getString("reason"));
|
||||
TEST_CHECK(first.getString("alternative_tie_status") == "estimated");
|
||||
TEST_CHECK(first.getNumber("interval_ns") == late.cost.primary);
|
||||
TEST_CHECK(first.getNumber("alternative_tie_interval_ns") == late.cost.alternative);
|
||||
TEST_CHECK(first.getObject("legacy_interval_window")->getString("status") == "unavailable");
|
||||
TEST_CHECK(first.getObject("legacy_interval_window")->getString("termination") == "event-budget");
|
||||
TEST_CHECK(llvm::json::Value(std::move(first)) == llvm::json::Value(std::move(second)));
|
||||
|
||||
auto blocked = evaluatePimExecution({{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 1}},
|
||||
{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 0}}}, target, options);
|
||||
TEST_CHECK(blocked.queue && blocked.cost.convergenceProved() == false);
|
||||
TEST_CHECK(blocked.queue->policies[1].termination == PimQueueTermination::NoReadyEvents);
|
||||
for (size_t failedPolicy = 0; failedPolicy < 2; ++failedPolicy) {
|
||||
auto asymmetric = *late.queue;
|
||||
asymmetric.policies[failedPolicy] = blocked.queue->policies[failedPolicy];
|
||||
auto cost = asymmetric.cost();
|
||||
cost.loweringSupport = spatial::StaticLoweringSupport::Supported;
|
||||
cost.selectionMargin = 0.021;
|
||||
TEST_CHECK(cost.primary.has_value() == (failedPolicy != 0));
|
||||
TEST_CHECK(cost.alternative.has_value() == (failedPolicy != 1));
|
||||
TEST_CHECK(cost.convergenceProved() == false && !cost.eligible());
|
||||
TEST_CHECK(cost.reason == blocked.queue->policies[failedPolicy].recurrence.reason);
|
||||
auto report = serializePimQueueEvaluation(asymmetric);
|
||||
TEST_CHECK(report.getString("status") == (failedPolicy == 0 ? "unavailable" : "estimated"));
|
||||
TEST_CHECK(report.getString("alternative_tie_status") == (failedPolicy == 1 ? "unavailable" : "estimated"));
|
||||
TEST_CHECK(report.getObject(failedPolicy == 0 ? "periodic_state" : "alternative_periodic_state")
|
||||
->getString("status") == "unproved");
|
||||
}
|
||||
|
||||
options.intervalWindowEventBudget.reset();
|
||||
auto normal = evaluatePimExecution(program, target, options);
|
||||
options.workBreakdown = true;
|
||||
auto detailed = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(late.cost.primary == normal.cost.primary && normal.cost.primary == detailed.cost.primary);
|
||||
TEST_CHECK(late.cost.alternative == normal.cost.alternative && normal.cost.alternative == detailed.cost.alternative);
|
||||
TEST_CHECK(normal.queue->policies[0].progress.processedEvents
|
||||
<= detailed.queue->policies[0].progress.processedEvents);
|
||||
}
|
||||
|
||||
static void testSharedEngine(PimInstructionTiming target) {
|
||||
auto record = [](O op, int32_t peer = 0, int32_t reg = 0, int32_t count = 0, int32_t bytes = 0) {
|
||||
pim_binary::InstructionRecord r;
|
||||
r.opcode = op; r.r2OrImm = peer; r.generic1 = reg; r.generic2 = count; r.generic3 = bytes;
|
||||
return r;
|
||||
};
|
||||
auto vectorWork = getPimVectorWorkNs(target, 65, 1, 1, true);
|
||||
PimInstructionProgram instructions;
|
||||
instructions.instructions = {
|
||||
binary({record(O::setbw, 0, 8, 8), record(O::ld, 0, 0, 0, 65),
|
||||
record(O::vvadd, 0, 0, 0, 65), record(O::send, 1, 0, 0, 65), record(O::sync, 1, 0)}),
|
||||
binary({record(O::recv, 0, 0, 0, 65), record(O::wait, 0, 0, 1), record(O::st, 0, 0, 0, 33)})};
|
||||
instructions.arrayGroups.resize(2);
|
||||
PimExecutionProgram abstract(2);
|
||||
abstract[0] = {{E::Work, target.processorPeriodNs}, {E::Load, 65}, {E::Work, vectorWork},
|
||||
{E::Send, 65, 1}, {E::Signal, 0, 1, 0}};
|
||||
abstract[1] = {{E::Receive, 65, 0}, {E::Wait, 0, 0, 0, 1}, {E::Store, 33}};
|
||||
PimPeriodicOptions details;
|
||||
details.workBreakdown = true;
|
||||
PimExecutionProgram trace;
|
||||
details.executionTrace = &trace;
|
||||
auto reference = analyzePimInstructions(instructions, target, 2, details);
|
||||
TEST_CHECK(reference.getString("status") == "estimated");
|
||||
TEST_CHECK(!reference.getObject("memory_controller")->getObject("periodic_state"));
|
||||
details.executionTrace = nullptr;
|
||||
auto roundTrip = analyzePimExecution(trace, target, details);
|
||||
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
|
||||
TEST_CHECK(roundTrip.getObject("memory_controller")->getNumber(field)
|
||||
== reference.getObject("memory_controller")->getNumber(field));
|
||||
for (size_t resource = 0; resource < 7; ++resource)
|
||||
TEST_CHECK((*roundTrip.getArray("resource_service_ns"))[resource].getAsInteger()
|
||||
== (*reference.getArray("resource_service_ns"))[resource].getAsInteger());
|
||||
details.verifyPeriodicState = true;
|
||||
auto proof = analyzePimExecution(trace, target, details);
|
||||
TEST_CHECK(proof.getObject("memory_controller")->getString("status")
|
||||
== reference.getObject("memory_controller")->getString("status"));
|
||||
TEST_CHECK(proof.getObject("memory_controller")->getObject("legacy_interval_window")->getNumber("interval_ns")
|
||||
== reference.getObject("memory_controller")->getNumber("interval_ns"));
|
||||
TEST_CHECK(proof.getObject("memory_controller")->getNumber("interval_ns")
|
||||
== proof.getObject("memory_controller")->getObject("periodic_state")->getNumber("interval_ns"));
|
||||
TEST_CHECK(proof.getObject("memory_controller")->getObject("periodic_state"));
|
||||
details.verifyPeriodicState = false;
|
||||
auto actual = analyzePimExecution(abstract, target, details);
|
||||
for (size_t core = 0; core < 2; ++core)
|
||||
TEST_CHECK((*actual.getArray("request_order"))[core].getAsObject()->getString("identity")
|
||||
== (*reference.getArray("request_order"))[core].getAsObject()->getString("identity"));
|
||||
for (size_t core = 0; core < 2; ++core)
|
||||
TEST_CHECK((*actual.getArray("request_order"))[core].getAsObject()->getString("blocking_identity")
|
||||
== (*reference.getArray("request_order"))[core].getAsObject()->getString("blocking_identity"));
|
||||
for (const char *field : {"global_read_bytes", "global_write_bytes", "global_read_requests",
|
||||
"global_write_requests", "global_memory_service_ns"})
|
||||
TEST_CHECK(actual.getInteger(field) == reference.getInteger(field));
|
||||
auto *a = actual.getObject("memory_controller"), *b = reference.getObject("memory_controller");
|
||||
TEST_CHECK(a && b && a->getString("status") == "estimated");
|
||||
TEST_CHECK(a->getNumber("interval_ns") == b->getNumber("interval_ns"));
|
||||
TEST_CHECK(a->getNumber("alternative_tie_interval_ns") == b->getNumber("alternative_tie_interval_ns"));
|
||||
abstract[1].clear();
|
||||
abstract[0] = {{E::Load, 1}, {E::Load, 65}};
|
||||
auto ordered = analyzePimExecution(abstract, target, details);
|
||||
abstract[0] = {{E::Load, 65}, {E::Load, 1}};
|
||||
auto reversed = analyzePimExecution(abstract, target, details);
|
||||
TEST_CHECK(ordered.getInteger("global_read_bytes") == reversed.getInteger("global_read_bytes"));
|
||||
TEST_CHECK((*ordered.getArray("request_order"))[0].getAsObject()->getString("identity")
|
||||
!= (*reversed.getArray("request_order"))[0].getAsObject()->getString("identity"));
|
||||
// Preserve repeated request order, not a single larger request.
|
||||
abstract[0] = {{E::Repeat, 2, 0, 0, 0, {{E::Load, 1}, {E::Work, 4}}}};
|
||||
abstract[1].clear();
|
||||
actual = analyzePimExecution(abstract, target);
|
||||
TEST_CHECK(actual.getInteger("global_read_requests") == 2);
|
||||
TEST_CHECK(actual.getInteger("global_memory_service_ns") == 160);
|
||||
abstract[0] = {{E::Repeat, 1000000000000ull, 0, 0, 0, {{E::Load, 1}}}};
|
||||
actual = analyzePimExecution(abstract, target);
|
||||
TEST_CHECK(actual.getString("status") == "unavailable");
|
||||
abstract[0] = {{E::Repeat, 1000000000000ull, 0, 0, 0, {{E::Work, 4}}}};
|
||||
actual = analyzePimExecution(abstract, target);
|
||||
TEST_CHECK(actual.getObject("memory_controller")->getNumber("interval_ns") == 4000000000000.0);
|
||||
abstract[0] = {{E::Signal, 0, int64_t(1) << 32}};
|
||||
TEST_CHECK(analyzePimExecution(abstract, target).getString("status") == "unavailable");
|
||||
TEST_CHECK(getPimMatrixOutputBytes(target, 2, pim_binary::kMatrixWeightPrecisionBits, 3, 1)
|
||||
== target.matrixColumns * 2 / pim_binary::kMatrixWeightPrecisionBits * 3);
|
||||
E copy{E::Work, 4000};
|
||||
copy.workClass = E::LocalCopy;
|
||||
abstract[0] = {copy, {E::Load, 1}};
|
||||
auto charged = analyzePimExecution(abstract, target);
|
||||
PimPeriodicOptions sensitivity;
|
||||
sensitivity.ignoreLocalCopies = true;
|
||||
auto omitted = analyzePimExecution(abstract, target, sensitivity);
|
||||
TEST_CHECK(*charged.getObject("memory_controller")->getNumber("interval_ns")
|
||||
- *omitted.getObject("memory_controller")->getNumber("interval_ns") == 4000);
|
||||
}
|
||||
|
||||
static void testAccountingAndOverflow(PimInstructionTiming target) {
|
||||
auto record = [](O op, int64_t bytes) {
|
||||
pim_binary::InstructionRecord r;
|
||||
r.opcode = op; r.generic3 = bytes;
|
||||
return r;
|
||||
};
|
||||
PimPeriodicOptions options;
|
||||
options.scalarIssue = false;
|
||||
options.workBreakdown = true;
|
||||
auto setbw = record(O::setbw, 0);
|
||||
setbw.generic1 = setbw.generic2 = 8;
|
||||
auto matrixInstruction = record(O::mvmul, 0);
|
||||
matrixInstruction.r2OrImm = pim_binary::kMatrixWeightPrecisionBits;
|
||||
PimInstructionProgram reference;
|
||||
reference.instructions = {binary({setbw, matrixInstruction, record(O::vvadd, 65), record(O::vmv, 3), record(O::lmv, 65), record(O::lldi, 65)})};
|
||||
reference.arrayGroups = {{3}};
|
||||
E matrix{E::Work, getPimMatrixWorkNs(target, 8,
|
||||
getPimMatrixOutputBytes(target, 2, pim_binary::kMatrixWeightPrecisionBits, 3, 1))};
|
||||
matrix.workClass = E::Matrix;
|
||||
E vector{E::Work, getPimVectorWorkNs(target, 65, 1, 1, true)};
|
||||
vector.workClass = E::Vector;
|
||||
E vectorCopy{E::Work, getPimVectorWorkNs(target, 3, 1, 1, false)};
|
||||
vectorCopy.workClass = E::VectorCopy;
|
||||
E copy{E::Work, target.getLocalMemoryServiceNs(65, false)
|
||||
+ target.getLocalMemoryServiceNs(65, true) + target.processorPeriodNs};
|
||||
copy.workClass = E::LocalCopy;
|
||||
E fill{E::Work, target.getLocalMemoryServiceNs(65, true) + target.processorPeriodNs};
|
||||
fill.workClass = E::Fill;
|
||||
auto exact = analyzePimInstructions(reference, target, 2, options);
|
||||
auto abstract = analyzePimExecution({{matrix, vector, vectorCopy, copy, fill}}, target, options);
|
||||
TEST_CHECK(*abstract.getArray("resource_service_ns") == *exact.getArray("resource_service_ns"));
|
||||
TEST_CHECK(abstract.getObject("memory_controller")->getNumber("interval_ns")
|
||||
== exact.getObject("memory_controller")->getNumber("interval_ns"));
|
||||
auto repeated = analyzePimExecution({{{E::Repeat, 7, 0, 0, 0, {matrix, vector, vectorCopy, copy, fill}}}}, target, options);
|
||||
for (size_t i = 0; i < 7; ++i)
|
||||
TEST_CHECK(*(*repeated.getArray("resource_service_ns"))[i].getAsInteger()
|
||||
== 7 * *(*abstract.getArray("resource_service_ns"))[i].getAsInteger());
|
||||
auto nested = analyzePimExecution({{{E::Repeat, 1000000, 0, 0, 0,
|
||||
{{E::Repeat, 1000000, 0, 0, 0, {fill}}}}}}, target, options);
|
||||
TEST_CHECK(*(*nested.getArray("resource_service_ns"))[2].getAsInteger()
|
||||
== static_cast<int64_t>(1000000000000ull * fill.amount));
|
||||
TEST_CHECK(nested.getInteger("expanded_graph_events").value() < 10);
|
||||
PimExecutionProgram pattern(2), expanded(2);
|
||||
pattern[0] = {{E::Repeat, 3, 0, 0, 0, {{E::Load, 65},
|
||||
{E::Repeat, 5, 0, 0, 0, {matrix, vector, copy, fill}}}}};
|
||||
for (size_t repeat = 0; repeat < 3; ++repeat) {
|
||||
expanded[0].push_back({E::Load, 65});
|
||||
for (size_t work = 0; work < 5; ++work)
|
||||
expanded[0].insert(expanded[0].end(), {matrix, vector, copy, fill});
|
||||
}
|
||||
pattern[1] = expanded[1] = {{E::Work, 311}, {E::Load, 257}};
|
||||
auto compactCost = analyzePimExecution(pattern, target, options);
|
||||
auto expandedCost = analyzePimExecution(expanded, target, options);
|
||||
for (const char *policy : {"interval_ns", "alternative_tie_interval_ns"})
|
||||
TEST_CHECK(compactCost.getObject("memory_controller")->getNumber(policy)
|
||||
== expandedCost.getObject("memory_controller")->getNumber(policy));
|
||||
TEST_CHECK(*compactCost.getArray("resource_service_ns") == *expandedCost.getArray("resource_service_ns"));
|
||||
std::vector<E> body{{E::Load, 1}};
|
||||
for (size_t i = 0; i < 10000; ++i) {
|
||||
E event{E::Work, 1}; event.workClass = i % 2 ? E::Vector : E::Fill;
|
||||
body.push_back(event);
|
||||
}
|
||||
auto cachedWork = analyzePimExecution({{{E::Repeat, 10000, 0, 0, 0, body}}}, target, options);
|
||||
TEST_CHECK(cachedWork.getInteger("global_read_requests") == 10000);
|
||||
TEST_CHECK(cachedWork.getInteger("expanded_graph_events").value() < 100000);
|
||||
TEST_CHECK((*cachedWork.getArray("resource_service_ns"))[1].getAsInteger() == 50000000);
|
||||
TEST_CHECK((*cachedWork.getArray("resource_service_ns"))[2].getAsInteger() == 50000000);
|
||||
uint64_t maximum = std::numeric_limits<uint64_t>::max();
|
||||
for (auto program : std::vector<PimExecutionProgram>{
|
||||
{{{E::Work, maximum}}}, {{{E::Load, maximum}}},
|
||||
{{{E::Repeat, maximum, 0, 0, 0, {fill}}}},
|
||||
{{{E::Work, uint64_t(1) << 62}, {E::Work, uint64_t(1) << 62}}}})
|
||||
TEST_CHECK(analyzePimExecution(program, target).getString("status") == "unavailable");
|
||||
target.sharedMemoryReadLatencyNs = maximum;
|
||||
TEST_CHECK(analyzePimExecution({{{E::Load, 65}}}, target).getString("status") == "unavailable");
|
||||
}
|
||||
|
||||
static PimPeriodicOptions spatialDetails() {
|
||||
PimPeriodicOptions options;
|
||||
options.workBreakdown = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
static void testCoreAssignments() {
|
||||
MLIRContext context;
|
||||
context.loadDialect<func::FuncDialect, spatial::SpatialDialect>();
|
||||
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module->getBodyRegion());
|
||||
auto loc = module->getLoc();
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto empty = [&](std::optional<int32_t> id) {
|
||||
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{});
|
||||
if (id) compute->setAttr("coreId", builder.getI32IntegerAttr(*id));
|
||||
compute.getBody().push_back(new Block);
|
||||
builder.setInsertionPointToStart(&compute.getBody().front());
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
return compute;
|
||||
};
|
||||
auto first = empty(0);
|
||||
auto second = empty(1);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(*module)));
|
||||
auto target = hardware(1);
|
||||
auto reason = [&]() {
|
||||
auto report = analyzePimSpatialSchedule(*module, target);
|
||||
return report.getString("reason").value_or("").str();
|
||||
};
|
||||
TEST_CHECK(reason().find("physical core") == std::string::npos);
|
||||
second->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
TEST_CHECK(reason() == "duplicate physical core assignment: 0");
|
||||
TEST_CHECK(evaluatePimSpatialSchedule(*module, target).cost.loweringSupport
|
||||
== spatial::StaticLoweringSupport::Unsupported);
|
||||
second->removeAttr("coreId");
|
||||
TEST_CHECK(reason() == "missing physical core");
|
||||
TEST_CHECK(evaluatePimSpatialSchedule(*module, target).cost.loweringSupport
|
||||
== spatial::StaticLoweringSupport::Unknown);
|
||||
for (int32_t id : {-1, 2}) {
|
||||
second->setAttr("coreId", builder.getI32IntegerAttr(id));
|
||||
TEST_CHECK(reason() == "physical core ID is outside the target: " + std::to_string(id));
|
||||
}
|
||||
second.erase();
|
||||
builder.setInsertionPoint(function.getBody().front().getTerminator());
|
||||
auto batch = spatial::SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
|
||||
builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{});
|
||||
batch.getBody().push_back(new Block);
|
||||
batch.getBody().front().addArgument(builder.getIndexType(), loc);
|
||||
builder.setInsertionPointToStart(&batch.getBody().front());
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
TEST_CHECK(reason() == "missing physical batch cores");
|
||||
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({1, 1}));
|
||||
TEST_CHECK(reason() == "duplicate physical core assignment: 1");
|
||||
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({1, 0}));
|
||||
TEST_CHECK(reason() == "duplicate physical core assignment: 0");
|
||||
TEST_CHECK(evaluatePimSpatialSchedule(*module, target).cost.loweringSupport
|
||||
== spatial::StaticLoweringSupport::Unsupported);
|
||||
first.erase();
|
||||
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({1, 2}));
|
||||
TEST_CHECK(reason() == "physical core ID is outside the target: 2");
|
||||
}
|
||||
|
||||
static void testDependencyTokens() {
|
||||
PimDependencyTokens fresh, drained;
|
||||
TEST_CHECK(fresh.empty() && fresh.remaining().empty());
|
||||
drained.initialize(1);
|
||||
TEST_CHECK(drained.consume() == 0);
|
||||
TEST_CHECK(drained.empty() && drained.remaining().empty());
|
||||
// Empty queues have identical future behavior, regardless of their capacity.
|
||||
for (uint64_t time : {0, 7, 7, 123}) {
|
||||
fresh.push(time); drained.push(time);
|
||||
TEST_CHECK(fresh.remaining() == drained.remaining());
|
||||
TEST_CHECK(fresh.consume() == drained.consume());
|
||||
TEST_CHECK(fresh.empty() && drained.empty());
|
||||
}
|
||||
// Consumed prefixes and compaction cannot change FIFO contents or readiness.
|
||||
drained.initialize(3);
|
||||
TEST_CHECK(drained.consume() == 0);
|
||||
drained.push(11);
|
||||
TEST_CHECK(drained.consume() == 0);
|
||||
TEST_CHECK(drained.consume() == 0);
|
||||
drained.push(11);
|
||||
drained.push(12);
|
||||
for (uint64_t time : {11, 11, 12}) TEST_CHECK(drained.consume() == time);
|
||||
TEST_CHECK(drained.empty());
|
||||
drained.initialize(0);
|
||||
TEST_CHECK(drained.empty() && drained.remaining().empty());
|
||||
}
|
||||
|
||||
static void testReadOnlySpatial(PimInstructionTiming target, bool batched) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, scf::SCFDialect,
|
||||
tensor::TensorDialect, linalg::LinalgDialect, spatial::SpatialDialect>();
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto type = RankedTensorType::get({1, 64}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
Value constant = arith::ConstantOp::create(builder, loc,
|
||||
DenseElementsAttr::get(type, builder.getF32FloatAttr(2)));
|
||||
Operation *compute;
|
||||
if (batched) {
|
||||
compute = spatial::SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
|
||||
builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{constant});
|
||||
compute->setAttr("coreIds", builder.getDenseI32ArrayAttr({0, 1}));
|
||||
} else {
|
||||
compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{constant});
|
||||
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
}
|
||||
Block *body = new Block;
|
||||
compute->getRegion(0).push_back(body);
|
||||
if (batched) body->addArgument(builder.getIndexType(), loc);
|
||||
Value input = body->addArgument(type, loc);
|
||||
builder.setInsertionPointToStart(body);
|
||||
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
|
||||
Value upper = arith::ConstantIndexOp::create(builder, loc, 28);
|
||||
Value one = arith::ConstantIndexOp::create(builder, loc, 1);
|
||||
Value local = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
|
||||
auto loop = scf::ForOp::create(builder, loc, zero, upper, one);
|
||||
builder.setInsertionPointToStart(loop.getBody());
|
||||
auto add = spatial::SpatVAddOp::create(builder, loc, type, local, input);
|
||||
builder.setInsertionPointAfter(loop);
|
||||
spatial::SpatWaitOp::create(builder, loc, zero, zero);
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
std::string before, after;
|
||||
llvm::raw_string_ostream os(before); module.print(os);
|
||||
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
auto fast = analyzePimSpatialSchedule(module, target);
|
||||
PimPeriodicOptions proofOptions;
|
||||
proofOptions.verifyPeriodicState = true;
|
||||
auto typed = evaluatePimSpatialSchedule(module, target, proofOptions);
|
||||
TEST_CHECK(typed.cost.available() && typed.cost.convergenceProved() == true);
|
||||
TEST_CHECK(typed.cost.loweringSupport == spatial::StaticLoweringSupport::Supported);
|
||||
TEST_CHECK(typed.report.getBoolean("lowering_supported") == typed.cost.loweringSupported());
|
||||
TEST_CHECK(typed.report.getBoolean("convergence_proved") == typed.cost.convergenceProved());
|
||||
TEST_CHECK(!fast.getArray("request_order") && !fast.getArray("resource_service_ns"));
|
||||
TEST_CHECK(!fast.getObject("without_local_copy_work") && !fast.getArray("work_class_ns"));
|
||||
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
|
||||
TEST_CHECK(fast.getObject("memory_controller")->getNumber(field)
|
||||
== report.getObject("memory_controller")->getNumber(field));
|
||||
TEST_CHECK(report.getBoolean("safe_for_selection") == false);
|
||||
TEST_CHECK(report.getInteger("global_read_requests") == (batched ? 2 : 28));
|
||||
TEST_CHECK(report.getInteger("global_read_bytes") == (batched ? 2 : 28) * 64);
|
||||
TEST_CHECK(report.getInteger("constant_read_requests") == (batched ? 2 : 28));
|
||||
llvm::raw_string_ostream out(after); module.print(out);
|
||||
TEST_CHECK(before == after);
|
||||
add->setAttr("test.annotation", builder.getUnitAttr());
|
||||
report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
TEST_CHECK(report.getInteger("global_read_requests") == (batched ? 2 : 28));
|
||||
builder.setInsertionPoint(body->getTerminator());
|
||||
Value source = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 3, 1, 224}, builder.getF32Type());
|
||||
Value destination = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 1, 224, 3}, builder.getF32Type());
|
||||
linalg::TransposeOp::create(builder, loc, source, destination, ArrayRef<int64_t>{0, 2, 3, 1});
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
auto expectedCopyWork = 224 * getPimVectorWorkNs(target, 3, 1, 1, false) * (batched ? 2 : 1);
|
||||
auto *classes = report.getArray("work_class_ns");
|
||||
auto *columns = report.getArray("work_class_columns");
|
||||
TEST_CHECK(classes && columns && classes->size() == 6 && columns->size() == 6);
|
||||
const char *names[] = {"matrix", "vector", "local_copy", "fill", "vector_copy", "scalar_issue"};
|
||||
uint64_t totalWork = 0;
|
||||
for (size_t i = 0; i < classes->size(); ++i) {
|
||||
TEST_CHECK((*columns)[i].getAsString() == names[i]);
|
||||
totalWork += *(*classes)[i].getAsInteger();
|
||||
}
|
||||
TEST_CHECK((*classes)[E::VectorCopy].getAsInteger() == expectedCopyWork);
|
||||
TEST_CHECK((*classes)[E::LocalCopy].getAsInteger() == 0);
|
||||
TEST_CHECK((*classes)[E::ScalarIssue].getAsInteger() == 0);
|
||||
TEST_CHECK(totalWork == report.getInteger("compute_and_local_work_ns"));
|
||||
auto softmax = spatial::SpatSoftmaxOp::create(builder, loc, type, input);
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
TEST_CHECK(analyzePimSpatialSchedule(module, target, spatialDetails()).getString("status") == "unavailable");
|
||||
softmax.erase();
|
||||
source = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 0, 1, 224}, builder.getF32Type());
|
||||
destination = tensor::EmptyOp::create(builder, loc, ArrayRef<int64_t>{1, 1, 224, 0}, builder.getF32Type());
|
||||
linalg::TransposeOp::create(builder, loc, source, destination, ArrayRef<int64_t>{0, 2, 3, 1});
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
TEST_CHECK(report.getString("reason") == "zero-element transpose has no abstract timing contract");
|
||||
module.erase();
|
||||
}
|
||||
|
||||
static void testHostSlice(PimInstructionTiming target) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, scf::SCFDialect,
|
||||
tensor::TensorDialect, spatial::SpatialDialect>();
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto type = RankedTensorType::get({1, 3, 4, 4}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({type}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto batch = spatial::SpatScheduledComputeBatch::create(builder, loc, TypeRange{},
|
||||
builder.getI32IntegerAttr(2), ValueRange{}, ValueRange{function.getArgument(0)});
|
||||
batch->setAttr("coreIds", builder.getDenseI32ArrayAttr({0, 1}));
|
||||
Block *body = new Block;
|
||||
batch.getBody().push_back(body);
|
||||
body->addArgument(builder.getIndexType(), loc);
|
||||
Value input = body->addArgument(type, loc);
|
||||
builder.setInsertionPointToStart(body);
|
||||
auto offsets = getAsOpFoldResult(builder.getI64ArrayAttr({0, 0, 1, 0}));
|
||||
auto sizes = getAsOpFoldResult(builder.getI64ArrayAttr({1, 3, 1, 4}));
|
||||
auto strides = getAsOpFoldResult(builder.getI64ArrayAttr({1, 1, 1, 1}));
|
||||
auto slice = tensor::ExtractSliceOp::create(builder, loc, input, offsets, sizes, strides);
|
||||
spatial::SpatReluOp::create(builder, loc, slice.getType(), slice.getResult());
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(batch);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
TEST_CHECK(report.getInteger("global_read_requests") == 6);
|
||||
TEST_CHECK(report.getInteger("global_read_bytes") == 24);
|
||||
module.erase();
|
||||
}
|
||||
|
||||
static void testCapturedVectorWeights(PimInstructionTiming target) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
|
||||
spatial::SpatialDialect>();
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto type = RankedTensorType::get({1, 64}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
Value zero = arith::ConstantOp::create(builder, loc,
|
||||
DenseElementsAttr::get(type, builder.getF32FloatAttr(0)));
|
||||
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{},
|
||||
ValueRange{zero}, ValueRange{});
|
||||
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
Block *body = new Block;
|
||||
compute.getBody().push_back(body);
|
||||
Value weight = body->addArgument(type, loc);
|
||||
builder.setInsertionPointToStart(body);
|
||||
Value local = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
|
||||
spatial::SpatVAddOp::create(builder, loc, type, local, weight);
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
// Captured weight operands stay block arguments; codegen cannot fold this load to LLDI.
|
||||
TEST_CHECK(report.getInteger("global_read_requests") == 1);
|
||||
TEST_CHECK(report.getInteger("constant_read_bytes") == 64);
|
||||
module.erase();
|
||||
}
|
||||
|
||||
static void testPublicationAndOutput(PimInstructionTiming target) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
|
||||
spatial::SpatialDialect>();
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto inputType = RankedTensorType::get({1, 2, 3, 5}, builder.getF32Type());
|
||||
auto outputType = RankedTensorType::get({1, 4, 3, 5}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {outputType}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{outputType},
|
||||
ValueRange{}, ValueRange{});
|
||||
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
compute.getBody().push_back(new Block);
|
||||
builder.setInsertionPointToStart(&compute.getBody().front());
|
||||
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
|
||||
Value one = arith::ConstantIndexOp::create(builder, loc, 1);
|
||||
Value input = tensor::EmptyOp::create(builder, loc, inputType.getShape(), inputType.getElementType());
|
||||
auto concat = spatial::SpatConcatOp::create(builder, loc, outputType,
|
||||
builder.getI64IntegerAttr(1), ValueRange{input, input});
|
||||
auto slice = tensor::ExtractSliceOp::create(builder, loc, concat.getOutput(),
|
||||
getAsOpFoldResult(builder.getI64ArrayAttr({0, 0, 1, 0})),
|
||||
getAsOpFoldResult(builder.getI64ArrayAttr({1, 4, 1, 5})),
|
||||
getAsOpFoldResult(builder.getI64ArrayAttr({1, 1, 1, 1})));
|
||||
spatial::SpatHostStoreSyncOp::create(builder, loc, zero, one, zero, zero, slice.getResult());
|
||||
spatial::SpatWaitOp::create(builder, loc, one, one);
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{concat.getOutput()});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
auto reader = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{});
|
||||
reader->setAttr("coreId", builder.getI32IntegerAttr(1));
|
||||
reader.getBody().push_back(new Block);
|
||||
builder.setInsertionPointToStart(&reader.getBody().front());
|
||||
zero = arith::ConstantIndexOp::create(builder, loc, 0);
|
||||
one = arith::ConstantIndexOp::create(builder, loc, 1);
|
||||
spatial::SpatWaitOp::create(builder, loc, zero, one);
|
||||
spatial::SpatSyncOp::create(builder, loc, zero, one);
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(reader);
|
||||
func::ReturnOp::create(builder, loc, compute.getResults());
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
TEST_CHECK(report.getInteger("global_write_requests") == 5);
|
||||
TEST_CHECK(report.getInteger("global_write_bytes") == 80);
|
||||
auto *runs = (*report.getArray("request_order"))[0].getAsObject()->getArray("runs");
|
||||
TEST_CHECK(runs->size() == 2);
|
||||
TEST_CHECK((*(*runs)[0].getAsArray())[1].getAsInteger() == 60);
|
||||
TEST_CHECK((*(*runs)[1].getAsArray())[1].getAsInteger() == 5);
|
||||
TEST_CHECK((*(*runs)[1].getAsArray())[2].getAsInteger() == 4);
|
||||
module.erase();
|
||||
}
|
||||
|
||||
static void testUnrepresentableSpatial(PimInstructionTiming target) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
|
||||
scf::SCFDialect, spatial::SpatialDialect>();
|
||||
for (bool loopOverflow : {false, true}) {
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{}, ValueRange{}, ValueRange{});
|
||||
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
compute.getBody().push_back(new Block);
|
||||
builder.setInsertionPointToStart(&compute.getBody().front());
|
||||
if (loopOverflow) {
|
||||
Value lower = arith::ConstantIndexOp::create(builder, loc, std::numeric_limits<int64_t>::min());
|
||||
Value upper = arith::ConstantIndexOp::create(builder, loc, std::numeric_limits<int64_t>::max());
|
||||
Value step = arith::ConstantIndexOp::create(builder, loc, 1);
|
||||
scf::ForOp::create(builder, loc, lower, upper, step);
|
||||
} else {
|
||||
auto type = RankedTensorType::get({std::numeric_limits<int64_t>::max(), 2}, builder.getF32Type());
|
||||
Value input = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
|
||||
spatial::SpatReluOp::create(builder, loc, type, input);
|
||||
}
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
TEST_CHECK(report.getString("status") == "unavailable");
|
||||
TEST_CHECK(!report.getBoolean("safe_for_selection").value());
|
||||
module.erase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void testMatrixGeometry(PimInstructionTiming target) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
|
||||
spatial::SpatialDialect>();
|
||||
target.matrixCellBits = 2;
|
||||
for (bool encoded : {false, true})
|
||||
for (int64_t rows : {64, 128, 192})
|
||||
for (int64_t columns : {64, 128, 192, 256}) {
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto inputType = RankedTensorType::get({1, rows}, builder.getF32Type());
|
||||
auto weightType = RankedTensorType::get({rows, columns}, builder.getF32Type(),
|
||||
encoded ? builder.getStringAttr("unsupported-layout") : Attribute{});
|
||||
auto resultType = RankedTensorType::get({1, columns}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
Value weight = arith::ConstantOp::create(builder, loc,
|
||||
DenseElementsAttr::get(weightType, builder.getF32FloatAttr(1)));
|
||||
auto compute = spatial::SpatScheduledCompute::create(builder, loc, TypeRange{},
|
||||
ValueRange{weight}, ValueRange{});
|
||||
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
compute.getBody().push_back(new Block);
|
||||
Value captured = compute.getBody().front().addArgument(weightType, loc);
|
||||
builder.setInsertionPointToStart(&compute.getBody().front());
|
||||
Value input = tensor::EmptyOp::create(builder, loc, inputType.getShape(), inputType.getElementType());
|
||||
spatial::SpatVMMOp::create(builder, loc, resultType, captured, input);
|
||||
spatial::SpatYieldOp::create(builder, loc, ValueRange{});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
auto report = analyzePimSpatialSchedule(module, target);
|
||||
if (encoded || columns % target.matrixColumns || static_cast<uint64_t>(rows) > target.matrixRows)
|
||||
TEST_CHECK(report.getString("reason") == "unavailable physical matrix weight geometry");
|
||||
else TEST_CHECK(report.getString("status") != "unavailable");
|
||||
module.erase();
|
||||
}
|
||||
}
|
||||
|
||||
static void testOutputMultiplicity(PimInstructionTiming target) {
|
||||
MLIRContext context;
|
||||
context.loadDialect<arith::ArithDialect, func::FuncDialect, tensor::TensorDialect,
|
||||
spatial::SpatialDialect>();
|
||||
for (bool repeatedResult : {false, true}) {
|
||||
auto module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module.getBodyRegion());
|
||||
auto loc = module.getLoc();
|
||||
auto type = RankedTensorType::get({1, 64}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "test", builder.getFunctionType({}, {type, type}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
SmallVector<Type> results(repeatedResult ? 1 : 2, type);
|
||||
auto compute = spatial::SpatScheduledCompute::create(builder, loc, results, ValueRange{}, ValueRange{});
|
||||
compute->setAttr("coreId", builder.getI32IntegerAttr(0));
|
||||
compute.getBody().push_back(new Block);
|
||||
builder.setInsertionPointToStart(&compute.getBody().front());
|
||||
Value input = tensor::EmptyOp::create(builder, loc, type.getShape(), type.getElementType());
|
||||
Value output = spatial::SpatReluOp::create(builder, loc, type, input);
|
||||
SmallVector<Value> yielded(results.size(), output);
|
||||
spatial::SpatYieldOp::create(builder, loc, yielded);
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc, ValueRange{compute.getResult(0), compute.getResult(repeatedResult ? 0 : 1)});
|
||||
TEST_CHECK(succeeded(verify(module)));
|
||||
auto report = analyzePimSpatialSchedule(module, target, spatialDetails());
|
||||
if (repeatedResult)
|
||||
TEST_CHECK(report.getString("reason") == "unavailable indirect compute output collection");
|
||||
else {
|
||||
TEST_CHECK(report.getInteger("global_write_requests") == 2);
|
||||
TEST_CHECK(report.getInteger("global_write_bytes") == 128);
|
||||
auto *runs = (*report.getArray("request_order"))[0].getAsObject()->getArray("runs");
|
||||
TEST_CHECK((*(*runs)[0].getAsArray())[2].getAsInteger() == 2);
|
||||
}
|
||||
module.erase();
|
||||
}
|
||||
}
|
||||
|
||||
static void testPeriodicStateProof() {
|
||||
auto target = hardware(1);
|
||||
target.processorCount = 3;
|
||||
target.sharedMemoryReadReturnsPayload = false;
|
||||
target.localMemoryPeriodNs = 1;
|
||||
target.localMemoryReadLatencyCycles = 20;
|
||||
target.localMemoryWriteLatencyCycles = 30;
|
||||
target.transferWidthBytes = target.sharedMemoryWidthBytes = 32;
|
||||
target.sharedMemoryReadLatencyNs = 40;
|
||||
target.sharedMemoryWriteLatencyNs = 50;
|
||||
target.sharedMemoryRequestLatencyNs = target.sharedMemoryResponseLatencyNs = {6, 6, 6};
|
||||
target.interProcessorLatencyNs = {0, 3, 4, 3, 0, 3, 4, 3, 0};
|
||||
PimExecutionProgram program{
|
||||
{{E::Work, 377}, {E::Store, 875}, {E::Work, 545}, {E::Load, 367},
|
||||
{E::Work, 333}, {E::Store, 80}, {E::Signal, 0, 1}, {E::Wait, 0, 0, 0, 1}},
|
||||
{{E::Work, 924}, {E::Load, 312}, {E::Work, 70}, {E::Store, 434},
|
||||
{E::Work, 337}, {E::Load, 275}, {E::Signal, 0, 2}, {E::Wait, 0, 0, 0, 1}},
|
||||
{{E::Work, 158}, {E::Store, 565}, {E::Work, 126}, {E::Load, 245},
|
||||
{E::Work, 1124}, {E::Store, 367}, {E::Signal, 0, 0}, {E::Wait, 0, 0, 0, 1}}};
|
||||
auto baseline = analyzePimExecution(program, target);
|
||||
PimPeriodicOptions options;
|
||||
options.verifyPeriodicState = true;
|
||||
options.workBreakdown = true;
|
||||
auto verified = analyzePimExecution(program, target, options);
|
||||
auto *queue = verified.getObject("memory_controller");
|
||||
auto *legacy = queue->getObject("legacy_interval_window");
|
||||
TEST_CHECK(legacy);
|
||||
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
|
||||
TEST_CHECK(legacy->getNumber(field) == baseline.getObject("memory_controller")->getNumber(field));
|
||||
auto *state = queue->getObject("periodic_state");
|
||||
TEST_CHECK(state && state->getString("status") == "proved");
|
||||
TEST_CHECK(state->getInteger("period_generations") == 3);
|
||||
TEST_CHECK(state->getNumber("interval_ns") == 17485.0 / 3);
|
||||
TEST_CHECK(queue->getNumber("interval_ns") == state->getNumber("interval_ns"));
|
||||
TEST_CHECK(legacy->getNumber("interval_ns") != state->getNumber("interval_ns"));
|
||||
// A fast writer builds signal tokens. Repeated local intervals are insufficient.
|
||||
program = {{{E::Signal, 0, 1}, {E::Work, 10}},
|
||||
{{E::Wait, 0, 0, 0, 1}, {E::Work, 100}}};
|
||||
verified = analyzePimExecution(program, target, options);
|
||||
state = verified.getObject("memory_controller")->getObject("periodic_state");
|
||||
TEST_CHECK(state && state->getString("status") == "unproved");
|
||||
}
|
||||
|
||||
static void testCompleteSchedulerState() {
|
||||
auto target = hardware(1);
|
||||
PimPeriodicOptions options;
|
||||
options.verifyPeriodicState = true;
|
||||
auto verify = [&](const PimExecutionProgram &program, bool expected) {
|
||||
auto baseline = analyzePimExecution(program, target);
|
||||
auto typed = evaluatePimExecution(program, target, options);
|
||||
TEST_CHECK(typed.cost.convergenceProved() == expected);
|
||||
auto detailed = std::move(typed.report);
|
||||
auto *queue = detailed.getObject("memory_controller");
|
||||
TEST_CHECK(queue);
|
||||
TEST_CHECK(detailed.getString("status") == (typed.cost.available() ? "estimated" : "unavailable"));
|
||||
if (!typed.cost.available()) TEST_CHECK(detailed.getString("reason") == typed.cost.reason);
|
||||
if (expected) {
|
||||
TEST_CHECK(queue->getNumber("interval_ns") == typed.cost.primary);
|
||||
TEST_CHECK(queue->getNumber("alternative_tie_interval_ns") == typed.cost.alternative);
|
||||
} else {
|
||||
for (const char *field : {"interval_ns", "alternative_tie_interval_ns"})
|
||||
TEST_CHECK(queue->getNumber(field) == baseline.getObject("memory_controller")->getNumber(field));
|
||||
}
|
||||
size_t policy = 0;
|
||||
for (const char *field : {"periodic_state", "alternative_periodic_state"}) {
|
||||
auto *state = queue->getObject(field);
|
||||
TEST_CHECK(state && (state->getString("status") == "proved") == expected);
|
||||
const auto &evidence = typed.cost.recurrence[policy++];
|
||||
TEST_CHECK(evidence.proved() == expected);
|
||||
TEST_CHECK(evidence.intervalNs == state->getNumber("interval_ns"));
|
||||
if (expected) TEST_CHECK(state->getBoolean("complete_scheduler_state") == true);
|
||||
}
|
||||
return detailed;
|
||||
};
|
||||
verify({{{E::Work, 0}}}, false);
|
||||
auto steady = verify({{{E::Work, 100}}}, true);
|
||||
auto *state = steady.getObject("memory_controller")->getObject("periodic_state");
|
||||
TEST_CHECK(state->getInteger("period_generations") == 1);
|
||||
TEST_CHECK(state->getNumber("interval_ns") == 100);
|
||||
verify({{{E::Work, uint64_t{1} << 60}}}, true);
|
||||
// Simultaneous signals, drained token queues, and controller arrivals must
|
||||
// recur under each deterministic arbitration policy.
|
||||
verify({{{E::Work, 10}, {E::Signal, 0, 1}, {E::Wait, 0, 0, 0, 1}},
|
||||
{{E::Work, 10}, {E::Signal, 0, 0}, {E::Wait, 0, 0, 0, 1}}}, true);
|
||||
verify({{{E::Load, 65}, {E::Signal, 0, 1}, {E::Wait, 0, 0, 0, 1}},
|
||||
{{E::Load, 65}, {E::Signal, 0, 0}, {E::Wait, 0, 0, 0, 1}}}, true);
|
||||
// Repeated output intervals do not prove uniform progress on every core.
|
||||
auto unequal = verify({{{E::Work, 7}}, {{E::Work, 11}}}, false);
|
||||
TEST_CHECK(unequal.getObject("memory_controller")->getString("status") == "estimated");
|
||||
// The complete state retains growing dependency-token queues.
|
||||
auto backlog = verify({{{E::Signal, 0, 1}, {E::Work, 10}},
|
||||
{{E::Wait, 0, 0, 0, 1}, {E::Work, 100}}}, false);
|
||||
TEST_CHECK(backlog.getObject("memory_controller")->getString("status") == "estimated");
|
||||
// Both endpoints exist, but neither WAIT can receive its signal.
|
||||
auto deadlock = verify({{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 1}},
|
||||
{{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 0}}}, false);
|
||||
TEST_CHECK(deadlock.getObject("memory_controller")->getString("status") == "unavailable");
|
||||
verify({{{E::Send, 65, 1}, {E::Receive, 65, 1}},
|
||||
{{E::Send, 65, 0}, {E::Receive, 65, 0}}}, false);
|
||||
// An independent active core must not hide an inactive dependency cycle.
|
||||
verify({{{E::Work, 10}}, {{E::Wait, 0, 0, 0, 1}, {E::Signal, 0, 1}}}, false);
|
||||
auto overflow = analyzePimExecution({{{E::Work, uint64_t{1} << 62}}}, target, options);
|
||||
TEST_CHECK(overflow.getString("status") == "unavailable");
|
||||
}
|
||||
|
||||
int main() {
|
||||
testDependencyTokens();
|
||||
testCoreAssignments();
|
||||
testPeriodicStateProof();
|
||||
testCompleteSchedulerState();
|
||||
for (int64_t period : {1, 5}) {
|
||||
testLateRecurrenceAuthority(hardware(period));
|
||||
testTypedResult(hardware(period));
|
||||
testSharedEngine(hardware(period));
|
||||
testAccountingAndOverflow(hardware(period));
|
||||
testMatrixGeometry(hardware(period));
|
||||
testOutputMultiplicity(hardware(period));
|
||||
testHostSlice(hardware(period));
|
||||
testCapturedVectorWeights(hardware(period));
|
||||
testPublicationAndOutput(hardware(period));
|
||||
testUnrepresentableSpatial(hardware(period));
|
||||
testReadOnlySpatial(hardware(period), false);
|
||||
testReadOnlySpatial(hardware(period), true);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,259 @@
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/ScheduleEvaluation.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/ScheduledSpatialPasses.hpp"
|
||||
#include "llvm/Support/JSON.h"
|
||||
#include <cassert>
|
||||
#include "TestCheck.hpp"
|
||||
#include <limits>
|
||||
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
|
||||
#include "src/Accelerators/PIM/Passes/PIMPasses.h"
|
||||
#include "src/Accelerators/PIM/Compiler/PimPipelineSelection.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCodeGen.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCompilerUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
|
||||
#include "src/Accelerators/PIM/Common/Support/FileSystemUtils.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialTargetResources.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Pim/Passes/Transforms/Bufferization/OpBufferizationInterfaces.hpp"
|
||||
#include "mlir/Dialect/Arith/Transforms/BufferizableOpInterfaceImpl.h"
|
||||
#include "mlir/Dialect/Bufferization/Transforms/FuncBufferizableOpInterfaceImpl.h"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
#include "mlir/Dialect/Tensor/IR/Tensor.h"
|
||||
#include "mlir/Dialect/Arith/IR/Arith.h"
|
||||
#include "mlir/Dialect/MemRef/IR/MemRef.h"
|
||||
#include "mlir/Dialect/SCF/IR/SCF.h"
|
||||
#include "mlir/Dialect/Linalg/IR/Linalg.h"
|
||||
#include "mlir/Dialect/Affine/IR/AffineOps.h"
|
||||
#include "mlir/Dialect/Bufferization/IR/Bufferization.h"
|
||||
#include "mlir/Pass/PassManager.h"
|
||||
#include "mlir/IR/Verifier.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/MemoryBuffer.h"
|
||||
using namespace onnx_mlir;
|
||||
using namespace onnx_mlir::spatial;
|
||||
static WholeScheduleCost scheduleCost(double interval) {
|
||||
WholeScheduleCost cost;
|
||||
cost.primary = cost.alternative = interval;
|
||||
cost.selectionMargin = 0.021;
|
||||
cost.loweringSupport = StaticLoweringSupport::Supported;
|
||||
for (auto &recurrence : cost.recurrence)
|
||||
recurrence = {ScheduleRecurrenceEvidence::Proved, 1, interval, {}};
|
||||
return cost;
|
||||
}
|
||||
|
||||
static void testEmptyGraphPreparation() {
|
||||
using namespace mlir;
|
||||
MLIRContext context;
|
||||
context.loadDialect<func::FuncDialect, SpatialDialect>();
|
||||
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module->getBodyRegion());
|
||||
auto function = func::FuncOp::create(builder, module->getLoc(), "main_graph",
|
||||
builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
func::ReturnOp::create(builder, module->getLoc());
|
||||
SchedulingTarget target;
|
||||
target.processorCount = target.residentWeightCapacity = 1;
|
||||
target.interProcessorLatencyNs = {0};
|
||||
for (bool collectCandidates : {false, true}) {
|
||||
auto prepared = prepareSpatialSchedules(*module, target, 1, collectCandidates);
|
||||
TEST_CHECK(succeeded(prepared) && prepared->candidates.size() == 1);
|
||||
TEST_CHECK(prepared->candidates.front().dominanceOrderCompute.empty());
|
||||
}
|
||||
}
|
||||
|
||||
static void testSelectedLoweringFailure() {
|
||||
using namespace mlir;
|
||||
MLIRContext context(MLIRContext::Threading::DISABLED);
|
||||
context.loadDialect<SpatialDialect, pim::PimDialect, func::FuncDialect,
|
||||
arith::ArithDialect, tensor::TensorDialect, memref::MemRefDialect,
|
||||
scf::SCFDialect, linalg::LinalgDialect, affine::AffineDialect,
|
||||
bufferization::BufferizationDialect>();
|
||||
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module->getBodyRegion());
|
||||
auto loc = module->getLoc();
|
||||
auto type = RankedTensorType::get({1, 536870912}, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "main_graph", builder.getFunctionType({type}, {type}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto compute = SpatGraphCompute::create(builder, loc, TypeRange{type}, ValueRange{},
|
||||
ValueRange{function.getArgument(0)});
|
||||
compute.getBody().push_back(new Block);
|
||||
Value input = compute.getBody().front().addArgument(type, loc);
|
||||
builder.setInsertionPointToStart(&compute.getBody().front());
|
||||
Value result = SpatReluOp::create(builder, loc, type, input);
|
||||
SpatYieldOp::create(builder, loc, ValueRange{result});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc, compute.getResults());
|
||||
TEST_CHECK(succeeded(verify(*module)));
|
||||
SchedulingTarget target;
|
||||
target.processorCount = 2;
|
||||
target.residentWeightCapacity = 1;
|
||||
target.synchronizationRegisterCount = 64;
|
||||
target.interProcessorLatencyNs = {0, 2, 2, 0};
|
||||
target.averageInterProcessorLatencyNs = 2;
|
||||
target.estimateRealizedScheduleCost = [](ModuleOp, llvm::json::Object &) {
|
||||
TEST_CHECK(false && "single-candidate compilation must skip costs");
|
||||
return WholeScheduleCost{};
|
||||
};
|
||||
SpatialTargetResources resources{{128, 128}, 1, 2, 16};
|
||||
std::string diagnostics;
|
||||
ScopedDiagnosticHandler handler(&context, [&](Diagnostic &diagnostic) {
|
||||
diagnostics += diagnostic.str(); return success();
|
||||
});
|
||||
ScopedCompilerOutputSuppression silence;
|
||||
// Match the compiler's preloaded backend registry before nested pipelines run.
|
||||
PassManager backend(&context);
|
||||
addPimLoweringPasses(backend, resources, EmitPimCodegen, false);
|
||||
DialectRegistry registry;
|
||||
backend.getDependentDialects(registry);
|
||||
pim::registerOpBufferizationInterfaces(registry);
|
||||
arith::registerBufferizableOpInterfaceExternalModels(registry);
|
||||
bufferization::func_ext::registerBufferizableOpInterfaceExternalModels(registry);
|
||||
context.appendDialectRegistry(registry);
|
||||
context.loadAllAvailableDialects();
|
||||
for (bool collectCandidates : {false, true}) {
|
||||
OwningOpRef<ModuleOp> checkpoint = cast<ModuleOp>((*module)->clone());
|
||||
auto prepared = prepareSpatialSchedules(*checkpoint, target, 2, collectCandidates);
|
||||
TEST_CHECK(succeeded(prepared) && !prepared->candidates.empty());
|
||||
}
|
||||
PassManager manager(&context);
|
||||
manager.addPass(createPimPipelineSelectionPass(target, resources, 2, SpatialDataflowExportStage::None));
|
||||
TEST_CHECK(failed(manager.run(*module)));
|
||||
TEST_CHECK(diagnostics.find("backend selection failed") != std::string::npos);
|
||||
TEST_CHECK(diagnostics.find("outside representable range") != std::string::npos);
|
||||
}
|
||||
|
||||
static void testResourceRetries() {
|
||||
using namespace mlir;
|
||||
auto previousCores = coresCount.getValue();
|
||||
auto previousSize = crossbarSize.getValue();
|
||||
auto previousCrossbars = crossbarCountInCore.getValue();
|
||||
coresCount = 2; crossbarSize = 128; crossbarCountInCore = 1;
|
||||
MLIRContext context;
|
||||
context.loadDialect<pim::PimDialect, memref::MemRefDialect, func::FuncDialect,
|
||||
arith::ArithDialect>();
|
||||
OwningOpRef<ModuleOp> checkpoint = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(checkpoint->getBodyRegion());
|
||||
auto loc = checkpoint->getLoc();
|
||||
auto function = func::FuncOp::create(builder, loc, "main_graph", builder.getFunctionType({}, {}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto core = pim::PimCoreOp::create(builder, loc, ValueRange{}, uint32_t{0});
|
||||
core.getBody().push_back(new Block);
|
||||
builder.setInsertionPointToStart(&core.getBody().front());
|
||||
auto allocation = memref::AllocOp::create(builder, loc,
|
||||
MemRefType::get({int64_t{1} << 31}, builder.getI8Type()));
|
||||
auto smallType = MemRefType::get({1}, builder.getI8Type());
|
||||
auto destination = memref::AllocOp::create(builder, loc, smallType);
|
||||
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
|
||||
pim::PimMemCopyOp::create(builder, loc, smallType, zero, zero, destination,
|
||||
allocation, builder.getI32IntegerAttr(1));
|
||||
pim::PimHaltOp::create(builder, loc);
|
||||
builder.setInsertionPointAfter(core);
|
||||
func::ReturnOp::create(builder, loc);
|
||||
TEST_CHECK(succeeded(verify(*checkpoint)));
|
||||
std::string before;
|
||||
llvm::raw_string_ostream stream(before); checkpoint->print(stream, OpPrintingFlags().printGenericOpForm());
|
||||
auto original = scheduleCost(100);
|
||||
auto faster = scheduleCost(90);
|
||||
std::vector<WholeScheduleCost> costs{original, faster};
|
||||
std::string reason;
|
||||
SpatialTargetResources resources{{128, 128}, 1, 2, 16};
|
||||
PassManager backend(&context);
|
||||
addPimLoweringPasses(backend, resources, EmitPimCodegen, false);
|
||||
DialectRegistry registry;
|
||||
backend.getDependentDialects(registry);
|
||||
pim::registerOpBufferizationInterfaces(registry);
|
||||
arith::registerBufferizableOpInterfaceExternalModels(registry);
|
||||
bufferization::func_ext::registerBufferizableOpInterfaceExternalModels(registry);
|
||||
context.appendDialectRegistry(registry);
|
||||
context.loadAllAvailableDialects();
|
||||
auto emit = [&](ModuleOp module) {
|
||||
llvm::SmallString<128> temporary;
|
||||
TEST_CHECK(!llvm::sys::fs::createUniqueDirectory("raptor-retry-program", temporary));
|
||||
std::string output = temporary.str().str();
|
||||
ScopedCompilerOutputSuppression silence;
|
||||
TEST_CHECK(compileToPimCode(module, output) == CompilerSuccess);
|
||||
auto binary = llvm::MemoryBuffer::getFile(output + "/core_0.pim");
|
||||
TEST_CHECK(binary && (*binary)->getBufferSize() > 12);
|
||||
std::string program = (*binary)->getBuffer().str();
|
||||
TEST_CHECK(!llvm::sys::fs::remove_directories(temporary));
|
||||
return program;
|
||||
};
|
||||
std::string emitted;
|
||||
for (bool allFail : {false, true}) {
|
||||
std::vector<size_t> attempted;
|
||||
OwningOpRef<ModuleOp> winner;
|
||||
std::vector<llvm::json::Object> summaries;
|
||||
auto selected = lowerSelectedPimCandidate(costs, [&](size_t index) {
|
||||
attempted.push_back(index);
|
||||
IRMapping mapping;
|
||||
OwningOpRef<ModuleOp> candidate = cast<ModuleOp>((*checkpoint)->clone(mapping));
|
||||
if (!index && !allFail)
|
||||
cast<memref::AllocOp>(mapping.lookup(allocation.getResult()).getDefiningOp())
|
||||
.getResult().setType(MemRefType::get({16}, builder.getI8Type()));
|
||||
llvm::json::Object summary;
|
||||
auto result = lowerPimPipelineCandidate(*candidate, resources, summary);
|
||||
if (result == PimCandidateResult::Success) winner = std::move(candidate);
|
||||
summaries.push_back(std::move(summary));
|
||||
return result;
|
||||
}, reason);
|
||||
TEST_CHECK(attempted == std::vector<size_t>({1, 0}));
|
||||
TEST_CHECK(allFail ? !selected : selected == 0);
|
||||
TEST_CHECK(summaries.front().getString("status") == "resource-limit");
|
||||
TEST_CHECK(summaries.front().getString("reason")->contains("signed int32 address range"));
|
||||
if (selected) emitted = emit(*winner);
|
||||
if (allFail) TEST_CHECK(reason.find("all candidate schedules failed") != std::string::npos);
|
||||
std::string after;
|
||||
llvm::raw_string_ostream out(after); checkpoint->print(out, OpPrintingFlags().printGenericOpForm());
|
||||
TEST_CHECK(before == after);
|
||||
}
|
||||
TEST_CHECK(!emitted.empty());
|
||||
IRMapping mapping;
|
||||
OwningOpRef<ModuleOp> alternative = cast<ModuleOp>((*checkpoint)->clone(mapping));
|
||||
cast<memref::AllocOp>(mapping.lookup(allocation.getResult()).getDefiningOp())
|
||||
.getResult().setType(MemRefType::get({16}, builder.getI8Type()));
|
||||
llvm::json::Object summary;
|
||||
TEST_CHECK(lowerPimPipelineCandidate(*alternative, resources, summary) == PimCandidateResult::Success);
|
||||
TEST_CHECK(emit(*alternative) == emitted);
|
||||
size_t attempts = 0;
|
||||
auto empty = lowerSelectedPimCandidate({}, [&](size_t) {
|
||||
++attempts;
|
||||
return PimCandidateResult::Success;
|
||||
}, reason);
|
||||
TEST_CHECK(!empty && attempts == 0 && reason.find("at least one candidate") != std::string::npos);
|
||||
auto selected = lowerSelectedPimCandidate(costs, [&](size_t) {
|
||||
++attempts;
|
||||
OwningOpRef<ModuleOp> invalid = cast<ModuleOp>((*alternative)->clone());
|
||||
invalid->walk([&](pim::PimMemCopyOp copy) {
|
||||
copy->setAttr("size", builder.getI64IntegerAttr(1));
|
||||
});
|
||||
llvm::json::Object failure;
|
||||
auto result = lowerPimPipelineCandidate(*invalid, resources, failure);
|
||||
TEST_CHECK(result == PimCandidateResult::Failure);
|
||||
TEST_CHECK(failure.getString("status") == "systemic-or-unclassified-error");
|
||||
return result;
|
||||
}, reason);
|
||||
TEST_CHECK(!selected && attempts == 1);
|
||||
TEST_CHECK(reason.find("recovery is not justified") != std::string::npos);
|
||||
for (unsigned unknown = 0; unknown < 4; ++unknown) {
|
||||
auto invalid = faster;
|
||||
if (unknown == 0) invalid.primary.reset();
|
||||
if (unknown == 1) invalid.recurrence[0].status = ScheduleRecurrenceEvidence::Unproved;
|
||||
if (unknown == 2) invalid.loweringSupport = StaticLoweringSupport::Unknown;
|
||||
if (unknown == 3) invalid.loweringSupport = StaticLoweringSupport::Unsupported;
|
||||
attempts = 0;
|
||||
std::vector<WholeScheduleCost> incomplete{original, invalid};
|
||||
selected = lowerSelectedPimCandidate(incomplete, [&](size_t) {
|
||||
++attempts;
|
||||
return PimCandidateResult::Success;
|
||||
}, reason);
|
||||
TEST_CHECK(!selected && attempts == 0);
|
||||
}
|
||||
coresCount = previousCores; crossbarSize = previousSize; crossbarCountInCore = previousCrossbars;
|
||||
}
|
||||
|
||||
int main() {
|
||||
testEmptyGraphPreparation();
|
||||
testSelectedLoweringFailure();
|
||||
testResourceRetries();
|
||||
mlir::MLIRContext context;
|
||||
context.allowUnregisteredDialects();
|
||||
mlir::OwningOpRef<mlir::ModuleOp> checkpoint = mlir::ModuleOp::create(mlir::UnknownLoc::get(&context));
|
||||
@@ -25,56 +273,52 @@ int main() {
|
||||
mlir::OwningOpRef<mlir::ModuleOp> trial = mlir::cast<mlir::ModuleOp>((*checkpoint)->clone(mapping));
|
||||
auto remapped = spatial::remapSpatialSchedule(schedule, mapping);
|
||||
auto copy = remapped.dominanceOrderCompute.front();
|
||||
assert(copy.op != op && copy.laneStart == 3 && copy.laneCount == 2);
|
||||
assert(remapped.computeToCpuMap.lookup(copy) == 7 && remapped.computeToCpuSlotMap.lookup(copy) == 4);
|
||||
assert(remapped.computeToAestMap.lookup(copy) == 42 && remapped.isLastComputeOfCpu.contains(copy));
|
||||
assert(remapped.cpuToLastComputeMap.lookup(7) == copy);
|
||||
TEST_CHECK(copy.op != op && copy.laneStart == 3 && copy.laneCount == 2);
|
||||
TEST_CHECK(remapped.computeToCpuMap.lookup(copy) == 7 && remapped.computeToCpuSlotMap.lookup(copy) == 4);
|
||||
TEST_CHECK(remapped.computeToAestMap.lookup(copy) == 42 && remapped.isLastComputeOfCpu.contains(copy));
|
||||
TEST_CHECK(remapped.cpuToLastComputeMap.lookup(7) == copy);
|
||||
copy.op->erase();
|
||||
assert(schedule.dominanceOrderCompute.front().op == op && op->getBlock() == checkpoint->getBody());
|
||||
TEST_CHECK(schedule.dominanceOrderCompute.front().op == op && op->getBlock() == checkpoint->getBody());
|
||||
}
|
||||
|
||||
auto score = [](double value, std::string id) { return WholeScheduleCost{value, value, {}, id, 0.021}; };
|
||||
auto score = [](double value, std::string) {
|
||||
return scheduleCost(value);
|
||||
};
|
||||
std::string reason;
|
||||
auto original = score(100, "0"), faster = score(90, "1");
|
||||
for (auto support : {StaticLoweringSupport::Unknown,
|
||||
StaticLoweringSupport::Supported, StaticLoweringSupport::Unsupported}) {
|
||||
auto cost = faster;
|
||||
cost.loweringSupport = support;
|
||||
TEST_CHECK(cost.available());
|
||||
TEST_CHECK(cost.eligible() == (support == StaticLoweringSupport::Supported));
|
||||
TEST_CHECK(cost.loweringSupported().has_value() == (support != StaticLoweringSupport::Unknown));
|
||||
}
|
||||
auto incompleteProof = faster;
|
||||
incompleteProof.recurrence[1].periodGenerations = 0;
|
||||
TEST_CHECK(incompleteProof.available() && !incompleteProof.eligible());
|
||||
incompleteProof.recurrence[1] = {};
|
||||
TEST_CHECK(incompleteProof.convergenceProved() == false && !incompleteProof.eligible());
|
||||
incompleteProof.recurrence[0] = {};
|
||||
TEST_CHECK(!incompleteProof.convergenceProved().has_value());
|
||||
auto select = [&](std::initializer_list<WholeScheduleCost> values) {
|
||||
return selectPipelineCandidate(llvm::ArrayRef<WholeScheduleCost>(values), reason);
|
||||
};
|
||||
assert(select({original, faster}) == 1);
|
||||
assert(select({original, score(97, "2")}) == 0);
|
||||
assert(select({original, score(105, "2")}) == 0);
|
||||
TEST_CHECK(select({original, faster}) == 1);
|
||||
TEST_CHECK(select({original, score(97, "2")}) == 0);
|
||||
TEST_CHECK(select({original, score(105, "2")}) == 0);
|
||||
auto sensitive = faster; sensitive.alternative = 101;
|
||||
assert(select({original, sensitive}) == 0);
|
||||
assert(select({original, faster, score(91, "2")}) == 1);
|
||||
assert(select({original, faster, score(90, "1")}) == 1);
|
||||
TEST_CHECK(select({original, sensitive}) == 0);
|
||||
TEST_CHECK(select({original, faster, score(91, "2")}) == 1);
|
||||
TEST_CHECK(select({original, faster, score(90, "1")}) == 1);
|
||||
auto unavailable = WholeScheduleCost{};
|
||||
assert(select({unavailable, faster}) == 0);
|
||||
assert(select({original, unavailable}) == 0);
|
||||
TEST_CHECK(select({unavailable, faster}) == 0);
|
||||
TEST_CHECK(select({original, unavailable}) == 0);
|
||||
auto unknownUncertainty = faster;
|
||||
unknownUncertainty.relativeUncertainty.reset();
|
||||
assert(select({original, unknownUncertainty}) == 0);
|
||||
unknownUncertainty.selectionMargin.reset();
|
||||
TEST_CHECK(select({original, unknownUncertainty}) == 0);
|
||||
for (double value : {0., -1., std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()})
|
||||
assert(select({original, score(value, "2")}) == 0);
|
||||
for (int i = 0; i < 8; ++i) assert(select({original, faster}) == 1);
|
||||
TEST_CHECK(select({original, score(value, "2")}) == 0);
|
||||
for (int i = 0; i < 8; ++i) TEST_CHECK(select({original, faster}) == 1);
|
||||
|
||||
// The generic query accepts target-lowered IR without a target dialect API.
|
||||
for (double interval : {90., 97.}) {
|
||||
SchedulingTarget target;
|
||||
assert(!target.prepareScheduleCost);
|
||||
target.prepareScheduleCost = [interval](mlir::ModuleOp module,
|
||||
llvm::json::Object &) -> mlir::FailureOr<ScheduleCostQuery> {
|
||||
assert(module.getBody()->getOperations().size() == 1);
|
||||
ScheduleCostQuery query;
|
||||
query.identity = "static-program";
|
||||
query.evaluate = [interval](llvm::json::Object &) {
|
||||
return WholeScheduleCost{interval, interval, {}, "static-program", 0.021};
|
||||
};
|
||||
return query;
|
||||
};
|
||||
llvm::json::Object summary;
|
||||
auto query = target.prepareScheduleCost(*checkpoint, summary);
|
||||
assert(mlir::succeeded(query) && query->identity == "static-program");
|
||||
auto cost = query->evaluate(summary);
|
||||
assert(cost.eligible() && select({original, cost}) == (interval == 90. ? 1 : 0));
|
||||
assert(checkpoint->getBody()->front().getName().getStringRef() == "test.compute");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#include "TestCheck.hpp"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
#include "mlir/IR/Diagnostics.h"
|
||||
#include "mlir/IR/Verifier.h"
|
||||
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/Scheduling/ComputeGraph.hpp"
|
||||
#include "src/Accelerators/PIM/Dialect/Spatial/Passes/Transforms/MergeComputeNodes/ScheduledSpatialPasses.hpp"
|
||||
#include <limits>
|
||||
|
||||
using namespace mlir;
|
||||
using namespace onnx_mlir;
|
||||
using namespace onnx_mlir::spatial;
|
||||
|
||||
static void checkProjection(ArrayRef<int64_t> shape, ArrayRef<int64_t> sizes,
|
||||
ArrayRef<int64_t> strides, bool available, bool valid = true) {
|
||||
MLIRContext context(MLIRContext::Threading::DISABLED);
|
||||
context.loadDialect<SpatialDialect, tensor::TensorDialect, func::FuncDialect>();
|
||||
OwningOpRef<ModuleOp> module = ModuleOp::create(UnknownLoc::get(&context));
|
||||
OpBuilder builder(module->getBodyRegion());
|
||||
auto loc = module->getLoc();
|
||||
auto sourceType = RankedTensorType::get(shape, builder.getF32Type());
|
||||
auto resultType = RankedTensorType::get(sizes, builder.getF32Type());
|
||||
auto function = func::FuncOp::create(builder, loc, "projection",
|
||||
builder.getFunctionType({sourceType}, {resultType}));
|
||||
builder.setInsertionPointToStart(function.addEntryBlock());
|
||||
auto compute = createEmptySpatGraphCompute(builder, loc, TypeRange{resultType},
|
||||
ValueRange{}, ValueRange{function.getArgument(0)});
|
||||
auto attrs = [&](ArrayRef<int64_t> values) {
|
||||
SmallVector<OpFoldResult> result;
|
||||
for (int64_t value : values) result.push_back(builder.getIndexAttr(value));
|
||||
return result;
|
||||
};
|
||||
auto slice = tensor::ExtractSliceOp::create(builder, loc, resultType,
|
||||
*compute.getInputArgument(0), attrs(SmallVector<int64_t>(shape.size(), 0)),
|
||||
attrs(sizes), attrs(strides));
|
||||
Value result = SpatReluOp::create(builder, loc, resultType, slice);
|
||||
SpatYieldOp::create(builder, loc, ValueRange{result});
|
||||
builder.setInsertionPointAfter(compute);
|
||||
func::ReturnOp::create(builder, loc, compute.getResults());
|
||||
std::string diagnostics;
|
||||
ScopedDiagnosticHandler handler(&context, [&](Diagnostic &diagnostic) {
|
||||
diagnostics += diagnostic.str();
|
||||
return success();
|
||||
});
|
||||
bool verified = succeeded(verify(*module));
|
||||
if (verified != valid) std::fprintf(stderr, "%s\n", diagnostics.c_str());
|
||||
TEST_CHECK(verified == valid);
|
||||
if (!valid) return;
|
||||
diagnostics.clear();
|
||||
for (Cost width : {1, 16}) {
|
||||
diagnostics.clear();
|
||||
SchedulingTarget target;
|
||||
target.processorCount = 1;
|
||||
target.sharedMemoryWidthBytes = width;
|
||||
target.sharedMemoryReadLatencyNs = width == 1 ? 7 : 3;
|
||||
auto graph = buildComputeGraph(function, target);
|
||||
TEST_CHECK(succeeded(graph));
|
||||
if (available) {
|
||||
TEST_CHECK(graph->baselineMemory.readRequests == 2);
|
||||
TEST_CHECK(graph->baselineMemory.readBytes == 4);
|
||||
TEST_CHECK(graph->baselineMemory.serviceNs == (width == 1 ? 28 : 6));
|
||||
TEST_CHECK(diagnostics.empty());
|
||||
} else {
|
||||
TEST_CHECK(graph->baselineMemory.readRequests == static_cast<Cost>(resultType.getNumElements()));
|
||||
TEST_CHECK(graph->baselineMemory.readBytes == static_cast<Cost>(resultType.getNumElements()));
|
||||
TEST_CHECK(graph->baselineMemory.serviceNs == resultType.getNumElements()
|
||||
* target.sharedMemoryReadLatencyNs);
|
||||
auto message = diagnostics.find("projection memory cost unavailable");
|
||||
TEST_CHECK(message != std::string::npos);
|
||||
TEST_CHECK(diagnostics.find("projection memory cost unavailable", message + 1) == std::string::npos);
|
||||
}
|
||||
// The optional estimate must not make otherwise verified Spatial IR fail
|
||||
// schedule preparation. Target lowering retains its own legality checks.
|
||||
target.residentWeightCapacity = 1;
|
||||
target.interProcessorLatencyNs = {0};
|
||||
function.setName("main_graph");
|
||||
auto schedules = prepareSpatialSchedules(*module, target, 1, false);
|
||||
TEST_CHECK(succeeded(schedules) && schedules->candidates.size() == 1);
|
||||
IRMapping mapping;
|
||||
OwningOpRef<ModuleOp> scheduled = cast<ModuleOp>(module->getOperation()->clone(mapping));
|
||||
auto schedule = remapSpatialSchedule(schedules->candidates.front(), mapping);
|
||||
auto realized = realizeSpatialSchedule(*scheduled, schedule, target, 1,
|
||||
SpatialDataflowExportStage::None);
|
||||
if (failed(realized)) std::fprintf(stderr, "%s\n", diagnostics.c_str());
|
||||
TEST_CHECK(succeeded(realized));
|
||||
TEST_CHECK(succeeded(verify(*scheduled)));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int64_t limit = std::numeric_limits<int64_t>::max();
|
||||
checkProjection({3, 5}, {2, 2}, {1, 1}, true);
|
||||
// These tensor types and slices are valid Spatial IR. The cost query cannot
|
||||
// represent their flattened offsets or strides with signed 64-bit values.
|
||||
checkProjection({2, limit}, {2, 2}, {1, 1}, false);
|
||||
checkProjection({1, limit, 2}, {1, 1, 1}, {1, 1, 1}, false);
|
||||
checkProjection({2, limit}, {1, 1}, {2, 1}, false);
|
||||
checkProjection({3, 5}, {4, 2}, {1, 1}, false, false);
|
||||
}
|
||||
@@ -1048,6 +1048,7 @@ static void checkPublicationMemoryTraffic(mlir::func::FuncOp function,
|
||||
auto report = getPeriodicScheduleReport(graph, *materialized, *shared, *boundaries, timed);
|
||||
assert(report.getString("status") == "estimated");
|
||||
assert(report.getBoolean("diagnostic_only") == true);
|
||||
assert(report.getBoolean("selection_eligible") == false);
|
||||
assert(report.getInteger("boundary_write_bytes") == expected.writeBytes);
|
||||
assert(report.getInteger("boundary_read_bytes") == expected.readBytes);
|
||||
timed.sharedMemoryReadReturnsPayload = false;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#define TEST_CHECK(condition) do { \
|
||||
if (!(condition)) { \
|
||||
std::fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #condition); \
|
||||
std::exit(EXIT_FAILURE); \
|
||||
} \
|
||||
} while (false)
|
||||
@@ -53,9 +53,7 @@ def main():
|
||||
report = json.loads((out / "reports/pipeline_selection.json").read_text())
|
||||
assert report["generated_candidates"] == 1
|
||||
assert report["single_candidate_analysis_skipped"]
|
||||
assert report["analyzed_candidates"] == report["lowered_candidates"] == 0
|
||||
assert report["unavailable_estimates"] == 0
|
||||
assert not report["reused_lowered_candidate"]
|
||||
assert report["lowered_candidates"] == 1
|
||||
assert "no cost comparison required" in report["reason"]
|
||||
else:
|
||||
assert "intermediate emission retains the original schedule" in output
|
||||
@@ -83,6 +81,10 @@ def main():
|
||||
capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert (out / "reports/pipeline_periodic.json").exists()
|
||||
report = json.loads((out / "reports/pipeline_periodic.json").read_text())
|
||||
assert report["diagnostic_only"]
|
||||
assert not report["selection_eligible"]
|
||||
assert not (out / "pim").exists()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -69,8 +69,11 @@ def check_controller(run):
|
||||
controller=True, issue=True)
|
||||
reader = next(core for core in zero['memory_controller']['per_core'] if core['core'] == 0)
|
||||
assert reader['queue_wait_ns'] > 0 # Zero service still queues behind an active request.
|
||||
bounded = run([[scalar, scalar], [scalar, load(65536), scalar]], controller=True, issue=True)
|
||||
bounded = json.loads(run([[scalar, scalar], [scalar, load(65536), scalar]],
|
||||
controller=True, issue=True, valid=False).stdout)
|
||||
assert bounded['status'] == 'unavailable'
|
||||
assert bounded['memory_controller']['status'] == 'unavailable'
|
||||
assert 'periodic_state' not in bounded['memory_controller']
|
||||
shared = [[record(31, a=0, b=2), store(128), record(32, arg=1, a=1), record(32, arg=2, a=1)],
|
||||
[record(32, arg=0, a=0), record(31, a=1, b=1), load(128), record(29, arg=2, length=64)],
|
||||
[record(32, arg=0, a=0), record(31, a=1, b=1), load(128), record(30, arg=1, length=64)]]
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Spatial selection lowers one winner and rejects incomplete comparisons."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import onnx
|
||||
from onnx import TensorProto, helper
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--compiler", required=True)
|
||||
args = parser.parse_args()
|
||||
with tempfile.TemporaryDirectory(prefix="raptor-spatial-selection-") as temporary:
|
||||
root = Path(temporary)
|
||||
guard = root / "static-audit.so"
|
||||
subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC",
|
||||
str(REPO / "test/PIM/StaticSchedulingAudit.c"), "-o", str(guard)], check=True)
|
||||
env = dict(os.environ, LD_PRELOAD=str(guard), OMP_NUM_THREADS="1")
|
||||
iteration = 0
|
||||
|
||||
def compile(source, pipeline=2, emit="EmitPimCodegen", extra=(), succeeds=True):
|
||||
nonlocal iteration
|
||||
output = root / str(iteration)
|
||||
iteration += 1
|
||||
output.mkdir()
|
||||
command = [args.compiler, str(source), "-o", str(output / "model"), "--maccel=PIM",
|
||||
f"--{emit}", f"--pipeline={pipeline}", "--core-count=138", "--crossbar-count=128",
|
||||
"--crossbar-size=128", "--mlir-disable-threading", "--pim-detect-communication-deadlock",
|
||||
f"--pim-target-config={REPO}/validation/pimsim_configs/pimcomp/arch-b/throughput_config_1000ms.json"]
|
||||
command.extend(extra)
|
||||
result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=120)
|
||||
assert (result.returncode == 0) == succeeds, result.stdout + result.stderr
|
||||
path = output / "reports/pipeline_selection.json"
|
||||
report = json.loads(path.read_text()) if path.exists() else None
|
||||
program = {str(p.relative_to(output / "pim")): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in (output / "pim").rglob("*") if p.is_file()}
|
||||
if "--pim-export-spatial-dataflow=all" in extra:
|
||||
for stage in ("spatial3_scheduled_no_comm", "spatial4_scheduled"):
|
||||
for suffix in ("nodes.csv", "edges.csv"):
|
||||
assert (output / f"reports/{stage}.{suffix}").stat().st_size > 0
|
||||
assert (output / "dialects/spatial4_scheduled.mlir").exists()
|
||||
assert (output / "dialects/pim1_buff.mlir").exists()
|
||||
shutil.rmtree(output)
|
||||
return report, program, result.stdout + result.stderr
|
||||
|
||||
vgg = REPO / "validation/networks/pimcomp_models/vgg8/vgg8-mnist-reconstructed.onnx"
|
||||
selected, program, _ = compile(vgg)
|
||||
count = selected["generated_candidates"]
|
||||
assert count > 1 and selected["strategy"] == "spatial-queue-aware"
|
||||
assert selected["lowered_candidates"] == 1
|
||||
assert len(selected["backend_attempts"]) == 1
|
||||
assert selected["backend_attempts"][0]["candidate"] == selected["selection"]
|
||||
assert not selected["prediction_error_bound_available"]
|
||||
assert selected["selection_margin"] == 0.021
|
||||
for candidate in selected["candidates"]:
|
||||
assert candidate["cost_available"] and candidate["convergence_proved"]
|
||||
assert candidate["lowering_supported"] and candidate["eligible"]
|
||||
assert "request_order" not in candidate["spatial_cost"]
|
||||
assert "without_local_copy_work" not in candidate["spatial_cost"]
|
||||
diagnostic, diagnostic_program, _ = compile(vgg, extra=(
|
||||
"--pim-export-spatial-dataflow=all", "--pim-dump-ir", "--pim-periodic-diagnostics"))
|
||||
assert diagnostic_program == program and diagnostic["selection"] == selected["selection"]
|
||||
|
||||
# A single schedule needs no optional timing contract.
|
||||
source = root / "softmax.onnx"
|
||||
tensor = helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 8])
|
||||
output = helper.make_tensor_value_info("y", TensorProto.FLOAT, [1, 8])
|
||||
model = helper.make_model(helper.make_graph([helper.make_node("Softmax", ["x"], ["y"], axis=1)],
|
||||
"softmax", [tensor], [output]), opset_imports=[helper.make_opsetid("", 18)])
|
||||
model.ir_version = 10
|
||||
onnx.save(model, source)
|
||||
selected, program, _ = compile(source)
|
||||
assert selected["single_candidate_analysis_skipped"] and selected["lowered_candidates"] == 1
|
||||
assert "spatial_cost" not in selected["candidates"][0]
|
||||
|
||||
# An unknown estimate cannot silently exclude any alternative.
|
||||
model = onnx.load(vgg)
|
||||
output = model.graph.output[0].name
|
||||
internal = output + "_before_softmax"
|
||||
for node in model.graph.node:
|
||||
for index, value in enumerate(node.output):
|
||||
if value == output:
|
||||
node.output[index] = internal
|
||||
for index, value in enumerate(node.input):
|
||||
if value == output:
|
||||
node.input[index] = internal
|
||||
model.graph.node.append(helper.make_node("Softmax", [internal], [output], axis=-1))
|
||||
source = root / "vgg-softmax.onnx"
|
||||
onnx.save(model, source)
|
||||
report, program, diagnostics = compile(source, succeeds=False)
|
||||
assert report["generated_candidates"] > 1 and report["status"] == "incomplete-costs"
|
||||
assert "cannot compare the complete candidate set" in diagnostics
|
||||
assert "unsupported abstract operation" in diagnostics
|
||||
assert not program and "backend_attempts" not in report
|
||||
# Latency PEFT and earlier emission do not invoke candidate costing.
|
||||
report, program, _ = compile(vgg, pipeline=1)
|
||||
assert report is None and program
|
||||
report, program, stderr = compile(vgg, emit="EmitSpatial")
|
||||
assert report is None and not program
|
||||
assert "intermediate emission retains the original schedule" in stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -13,16 +13,7 @@ REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
def check_selected_program(out):
|
||||
report = json.loads((out / "reports/pipeline_selection.json").read_text())
|
||||
program = json.loads((out / "pim/config.json").read_text())
|
||||
digest = hashlib.sha256()
|
||||
for core in range(program["core_cnt"]):
|
||||
digest.update((out / f"pim/core_{core}.pim").read_bytes())
|
||||
for count in program["array_group_map"][f"core{core}"]:
|
||||
digest.update(f"{count},".encode())
|
||||
digest.update(b";")
|
||||
assert digest.hexdigest().upper() == report["candidates"][report["selection"]]["identity"], \
|
||||
"emitted program differs from the analyzed winner"
|
||||
assert report["reused_lowered_candidate"]
|
||||
assert report["strategy"] == "spatial-queue-aware" and report["lowered_candidates"] == 1
|
||||
for name in ("spatial3_scheduled_no_comm.txt", "memory_report.txt"):
|
||||
assert (out / "reports" / name).stat().st_size > 0, name
|
||||
return report
|
||||
@@ -71,12 +62,13 @@ def main():
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
report = json.loads((out / "reports/pipeline_selection.json").read_text())
|
||||
assert report["static_analysis"] and report["temporary_disk_bytes"] == 0
|
||||
assert report["analyzed_candidates"] > 1, report
|
||||
assert report["generated_candidates"] > 1, report
|
||||
assert all("identity" not in c and "request_order" not in c["spatial_cost"]
|
||||
for c in report["candidates"])
|
||||
check_selected_program(out)
|
||||
assert not report["reconstructed_spatial_artifacts"]
|
||||
assert not (out / "dialects").exists()
|
||||
assert not list(out.glob("candidate*"))
|
||||
decisions.append((report["selection"], [(c["identity"], c.get("interval_ns"), c.get("alternative_tie_interval_ns"))
|
||||
decisions.append((report["selection"], [(c.get("interval_ns"), c.get("alternative_tie_interval_ns"))
|
||||
for c in report["candidates"]]))
|
||||
# Keep the test's disk peak to one selected program.
|
||||
shutil.rmtree(out)
|
||||
@@ -95,8 +87,9 @@ def main():
|
||||
result = subprocess.run(command, cwd=root, env=env, capture_output=True, text=True, timeout=90)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
report = check_selected_program(out)
|
||||
assert report["selection"] != 0 and report["reconstructed_spatial_artifacts"] == diagnostics
|
||||
identity = report["candidates"][report["selection"]]["identity"]
|
||||
assert report["selection"] != 0 and bool((out / "dialects").exists()) == diagnostics
|
||||
identity = {str(p.relative_to(out / "pim")): hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in (out / "pim").rglob("*") if p.is_file()}
|
||||
schedule = (out / "reports/spatial3_scheduled_no_comm.txt").read_bytes()
|
||||
if diagnostics:
|
||||
assert identity == previous_identity and schedule == previous_schedule
|
||||
@@ -123,9 +116,9 @@ def main():
|
||||
result = subprocess.run(command, cwd=root, env=env, capture_output=True, text=True, timeout=60)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
report = json.loads((out / "reports/pipeline_selection.json").read_text())
|
||||
assert report["selection"] == 0 and report["analyzed_candidates"] == 0
|
||||
assert report["generated_candidates"] == 1 and report["lowered_candidates"] == 0
|
||||
assert "ROB capacity one" in report["reason"] and report["unavailable_estimates"] > 0
|
||||
assert report["selection"] == 0 and report["single_candidate_analysis_skipped"]
|
||||
assert report["generated_candidates"] == 1 and report["lowered_candidates"] == 1
|
||||
assert "no cost comparison required" in report["reason"]
|
||||
assert report["temporary_disk_bytes"] == 0 and not list(root.glob("candidate*"))
|
||||
shutil.rmtree(out)
|
||||
out.mkdir()
|
||||
@@ -134,9 +127,9 @@ def main():
|
||||
result = subprocess.run(command, cwd=root, env=env, capture_output=True, text=True, timeout=60)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
report = json.loads((out / "reports/pipeline_selection.json").read_text())
|
||||
assert report["selection"] == 0 and report["analyzed_candidates"] == 0
|
||||
assert report["generated_candidates"] == 1 and report["lowered_candidates"] == 0
|
||||
assert "synchronization ablation" in report["reason"]
|
||||
assert report["selection"] == 0 and report["single_candidate_analysis_skipped"]
|
||||
assert report["generated_candidates"] == 1 and report["lowered_candidates"] == 1
|
||||
assert "no cost comparison required" in report["reason"]
|
||||
assert not (root / "executed").exists(), "production compiler executed a diagnostic or simulator"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Overflow must fail explicitly, including builds with disabled assertions."""
|
||||
import argparse
|
||||
import subprocess
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--test", required=True)
|
||||
args = parser.parse_args()
|
||||
result = subprocess.run([args.test, "--stride-overflow"], capture_output=True, text=True)
|
||||
assert result.returncode != 0
|
||||
assert "row-major strides are not representable in int64_t" in result.stderr, result.stderr
|
||||
assert "Assertion" not in result.stderr
|
||||
Reference in New Issue
Block a user