Logical vs Physical Plan
Tez exposes two planes, and confusing them is the root of most "why did my DAG do that" questions.
- Logical plan — what the application author writes:
Vertexes,Edges, each edge carrying anEdgeProperty. Lives intez-api. It says what connects to what and how data should move, but nothing about which task index reads from which. - Physical plan — what the AM actually schedules:
TaskImplinstances per vertex, per-edge routing decisions, container assignments. Lives intez-dag. Mutable at runtime throughVertexManagerreconfiguration andEdgeManagerPluginrouting.
The bridge between them is the EdgeManagerPlugin: it turns "these two
vertices are connected by a SCATTER_GATHER edge" into concrete answers to
"how many physical inputs does destination task 3 have, and which source output
does each carry."
After this chapter you can compute, for any DataMovementType, exactly how many
physical inputs and outputs a task sees; read the built-in edge managers and
explain their routing; and reason about what happens to routing when a
VertexManager changes parallelism mid-flight. This chapter is the physical
counterpart to ipo-abstractions.md, which covers the
per-IO contracts, and it feeds directly into shuffle-sort.md,
where those physical inputs become fetches.
The logical plane
ls tez-api/src/main/java/org/apache/tez/dag/api/ | grep -E "DAG|Vertex|Edge|EdgeProperty"
| Class | Purpose |
|---|---|
DAG | the builder — holds vertices, edges, vertex groups |
Vertex | logical vertex: a ProcessorDescriptor + a target parallelism |
Edge | logical edge between two vertices |
EdgeProperty | routing + scheduling + durability + the IO descriptors |
EdgeProperty — four orthogonal axes
grep -n "enum DataMovementType\|enum DataSourceType\|enum SchedulingType\|public static EdgeProperty create" \
tez-api/src/main/java/org/apache/tez/dag/api/EdgeProperty.java
// tez-api, org.apache.tez.dag.api.EdgeProperty
public enum DataMovementType { ONE_TO_ONE, BROADCAST, SCATTER_GATHER, CUSTOM }
public enum DataSourceType { PERSISTED, PERSISTED_RELIABLE, EPHEMERAL }
public enum SchedulingType { SEQUENTIAL, CONCURRENT }
| Axis | Values | Controls |
|---|---|---|
DataMovementType | ONE_TO_ONE, BROADCAST, SCATTER_GATHER, CUSTOM | how source outputs map to destination inputs |
DataSourceType | PERSISTED, PERSISTED_RELIABLE, EPHEMERAL | whether outputs survive a source task failure; drives re-execution policy |
SchedulingType | SEQUENTIAL, CONCURRENT | whether the destination may start before the source completes (needed for broadcast/pipelined shuffle) |
OutputDescriptor / InputDescriptor | class + payload | the IO classes wired on each end (see ipo-abstractions.md) |
A logical edge says nothing about which destination task index reads from
which source task index. That decision belongs entirely to the
EdgeManagerPlugin.
The physical plane
When the AM initializes a DAG it builds, per logical vertex:
ls tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/ | \
grep -E "VertexImpl|TaskImpl|TaskAttemptImpl|^Edge.java"
VertexImpl— the runtime vertex state machine (see vertex-lifecycle.md).TaskImpl[]— one per parallelism slot.TaskAttemptImpl— one per attempt of each task.Edge— the runtime edge object holding a liveEdgeManagerPlugin.
flowchart LR
subgraph logical[Logical]
LV1["Vertex A (parallelism 3)"]
LV2["Vertex B (parallelism 2)"]
LV1 -- "EdgeProperty SCATTER_GATHER" --> LV2
end
subgraph physical[Physical]
A0[A.0] --> B0[B.0]
A0 --> B1[B.1]
A1[A.1] --> B0
A1 --> B1
A2[A.2] --> B0
A2 --> B1
end
logical --> physical
Each source task produces one output partition per destination task; each
destination task reads one physical input per source task. The EdgeManager
decides which output partition goes to which input index.
The task-level view: TaskSpec
Before a task runs, the AM hands the container a TaskSpec — the physical plan,
serialized down to one task. This is the concrete artifact the two planes meet
in.
grep -n "private\|public TaskSpec(" \
tez-runtime-internals/src/main/java/org/apache/tez/runtime/api/impl/TaskSpec.java
// tez-runtime-internals, org.apache.tez.runtime.api.impl.TaskSpec
private TezTaskAttemptID taskAttemptId;
private String dagName;
private String vertexName;
private ProcessorDescriptor processorDescriptor;
private List<InputSpec> inputSpecList;
private List<OutputSpec> outputSpecList;
private List<GroupInputSpec> groupInputSpecList;
private int vertexParallelism = -1;
private Configuration taskConf;
Each InputSpec carries the physical edge count — the number the framework
passes to your AbstractLogicalInput(context, numPhysicalInputs) constructor:
// tez-runtime-internals, org.apache.tez.runtime.api.impl.InputSpec
private String sourceVertexName;
private InputDescriptor inputDescriptor;
private int physicalEdgeCount; // <- becomes numPhysicalInputs
OutputSpec mirrors it with destinationVertexName and physicalEdgeCount.
The physicalEdgeCount on an InputSpec is exactly
EdgeManagerPlugin.getNumDestinationTaskPhysicalInputs(destTaskIndex). That
is the seam: the AM asks the edge manager, stamps the answer into the
InputSpec, ships the TaskSpec, and the runtime constructs your input with it.
EdgeManagerPlugin — the routing brain
grep -n "public abstract" tez-api/src/main/java/org/apache/tez/dag/api/EdgeManagerPlugin.java
// tez-api, org.apache.tez.dag.api.EdgeManagerPlugin
public abstract void initialize() throws Exception;
public abstract int getNumDestinationTaskPhysicalInputs(int destinationTaskIndex) throws Exception;
public abstract int getNumSourceTaskPhysicalOutputs(int sourceTaskIndex) throws Exception;
public abstract void routeDataMovementEventToDestination(DataMovementEvent event,
int sourceTaskIndex, int sourceOutputIndex,
Map<Integer, List<Integer>> destinationTaskAndInputIndices) throws Exception;
public abstract void routeInputSourceTaskFailedEventToDestination(int sourceTaskIndex,
Map<Integer, List<Integer>> destinationTaskAndInputIndices) throws Exception;
public abstract int getNumDestinationConsumerTasks(int sourceTaskIndex) throws Exception;
public abstract int routeInputErrorEventToSource(InputReadErrorEvent event,
int destinationTaskIndex, int destinationFailedInputIndex) throws Exception;
The plugin is constructed with an EdgeManagerPluginContext, its only window
onto the DAG:
// tez-api, org.apache.tez.dag.api.EdgeManagerPluginContext
public UserPayload getUserPayload();
public String getSourceVertexName();
public String getDestinationVertexName();
public int getSourceVertexNumTasks();
public int getDestinationVertexNumTasks();
String getVertexGroupName();
Notice what is not here: the plugin cannot see task states, locations, or the DAG. It is a pure function of the two vertices' task counts plus its own payload. That constraint is deliberate — it is what lets the AM call these methods thousands of times per second during event routing without locking the DAG.
EdgeManagerPluginOnDemand — the fast path
The map-filling routeDataMovementEventToDestination is the legacy API. Modern
built-ins extend EdgeManagerPluginOnDemand, which returns compact
EventRouteMetadata / CompositeEventRouteMetadata instead of mutating a map —
the AM materializes per-destination events lazily, only when a destination task
actually starts.
grep -n "public abstract\|class EventRouteMetadata\|class CompositeEventRouteMetadata" \
tez-api/src/main/java/org/apache/tez/dag/api/EdgeManagerPluginOnDemand.java
// tez-api, org.apache.tez.dag.api.EdgeManagerPluginOnDemand
public abstract void prepareForRouting() throws Exception;
public abstract @Nullable EventRouteMetadata routeDataMovementEventToDestination(
int sourceTaskIndex, int sourceOutputIndex, int destinationTaskIndex) throws Exception;
public abstract @Nullable CompositeEventRouteMetadata routeCompositeDataMovementEventToDestination(
int sourceTaskIndex, int destinationTaskIndex) throws Exception;
Edge picks the routing mode at routingToBegin():
// tez-dag, org.apache.tez.dag.app.dag.impl.Edge.routingToBegin()
if (numDestTasks == 0) {
routingNeeded = false;
} else if (numDestTasks < 0) {
throw new TezUncheckedException("Internal error. Not expected to route events ... " +
"until parallelism is determined ...");
}
if (edgeManager instanceof EdgeManagerPluginOnDemand) {
onDemandRouting = true;
}
if (onDemandRouting) {
((EdgeManagerPluginOnDemand) edgeManager).prepareForRouting();
}
Note:
numDestTasks < 0throws. An edge cannot route until the destination vertex's parallelism is known. This is why auto-parallelism (below) must resolve before any event flows to the destination — the whole ordering is enforced right here.
The built-in edge managers
The three canonical routers live in tez-dag; the CUSTOM ones ship in
tez-runtime-library. Do not assume they are all in one place:
find tez-dag/src/main/java -name "*EdgeManager*.java"
find tez-runtime-library/src/main/java -name "*EdgeManager*.java"
| Plugin | Module | DataMovementType | Rule |
|---|---|---|---|
ScatterGatherEdgeManager | tez-dag | SCATTER_GATHER | dest d reads partition d from every source |
BroadcastEdgeManager | tez-dag | BROADCAST | every source output goes to every dest task |
OneToOneEdgeManager / OneToOneEdgeManagerOnDemand | tez-dag | ONE_TO_ONE | source i → dest i, counts must match |
CartesianProductEdgeManager* | tez-runtime-library | CUSTOM | grid routing for cross products |
FairShuffleEdgeManager | tez-runtime-library | CUSTOM | skew-aware shuffle |
SCATTER_GATHER
sed -n '55,65p;100,165p' \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/ScatterGatherEdgeManager.java
The counting methods are two lines each, and they are the whole story:
// tez-dag, org.apache.tez.dag.app.dag.impl.ScatterGatherEdgeManager
public int getNumDestinationTaskPhysicalInputs(int destinationTaskIndex) {
return getContext().getSourceVertexNumTasks(); // = source parallelism
}
public int getNumSourceTaskPhysicalOutputs(int sourceTaskIndex) {
return getContext().getDestinationVertexNumTasks(); // = dest parallelism
}
So for A (parallelism 3) → B (parallelism 2): every A task emits 2
partitions (one per B task); every B task has 3 physical inputs (one per
A task). The on-demand routing collapses to index equality:
// tez-dag, ScatterGatherEdgeManager.routeDataMovementEventToDestination (on-demand)
public EventRouteMetadata routeDataMovementEventToDestination(
int sourceTaskIndex, int sourceOutputIndex, int destinationTaskIndex) {
if (sourceOutputIndex == destinationTaskIndex) { // partition d -> task d
return getOrCreateCommonRouteMeta().get(sourceTaskIndex);
}
return null; // this output not for this dest
}
Invariant to memorize: numSourceOutputs == destParallelism and
numDestInputs == srcParallelism. Every DataMovementEvent a B task
receives corresponds to one A attempt's partition-d output.
ONE_TO_ONE
sed -n '45,90p' tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/OneToOneEdgeManager.java
// tez-dag, org.apache.tez.dag.app.dag.impl.OneToOneEdgeManager
public void routeDataMovementEventToDestination(DataMovementEvent event,
int sourceTaskIndex, int sourceOutputIndex,
Map<Integer, List<Integer>> destinationTaskAndInputIndices) {
checkState();
destinationTaskAndInputIndices.put(sourceTaskIndex, destinationInputIndices);
}
private void checkState() {
Preconditions.checkState(getContext().getSourceVertexNumTasks()
== getContext().getDestinationVertexNumTasks(),
"1-1 source and destination task counts must match. ...");
}
Source i feeds destination i, one physical input each,
getNumDestinationConsumerTasks returns 1. The checkState() precondition is
the trap: if anything changes source or destination parallelism asymmetrically
after the edge is live, this Preconditions.checkState throws. Never enable
auto-parallelism on a vertex whose incoming edge is ONE_TO_ONE.
BROADCAST
sed -n '40,120p' tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/BroadcastEdgeManager.java
// tez-dag, org.apache.tez.dag.app.dag.impl.BroadcastEdgeManager
public int getNumDestinationTaskPhysicalInputs(int destinationTaskIndex) {
return getContext().getSourceVertexNumTasks(); // every dest reads all sources
}
public void routeDataMovementEventToDestination(DataMovementEvent event,
int sourceTaskIndex, int sourceOutputIndex,
Map<Integer, List<Integer>> destinationTaskAndInputIndices) {
for (int i = 0; i < getContext().getDestinationVertexNumTasks(); ++i) {
// every source output is delivered to every destination task
}
}
Each source emits a single logical output consumed by every destination task.
Data volume amplification is srcParallelism × destParallelism — a 500-task
broadcast source into a 500-task destination replicates each byte 500 times.
Broadcasting a large vertex is an antipattern; keep broadcast sides small
(dimension tables, not facts).
CUSTOM — Cartesian product
find tez-runtime-library/src/main/java -path "*cartesianproduct*EdgeManager*.java"
CartesianProductVertexManager chunks each source's outputs and the
CartesianProductEdgeManagerReal projects a (sourceChunk, position) pair onto
a 2-D grid of destination tasks. CUSTOM is the escape hatch by which Hive
ships routing for unconventional joins that none of the three built-ins express.
The other two axes: durability and scheduling
DataMovementType gets all the attention, but the other EdgeProperty axes
change behavior, not just routing, and they interact with the physical plan.
DataSourceType governs what happens to a source's output when the source
attempt dies:
| Value | Meaning | Re-execution consequence |
|---|---|---|
PERSISTED | output written to local disk, survives the task but not the node | if the source node dies, the source attempt must re-run to regenerate the partition |
PERSISTED_RELIABLE | output persisted to reliable storage (e.g. HDFS) | survives node loss; consumers can re-fetch without re-running the source |
EPHEMERAL | output held in memory, never persisted | consumer must run concurrently with the source (implies CONCURRENT scheduling); any failure re-runs both |
This is why a fetch-failure storm (see
shuffle-sort.md)
on a PERSISTED edge triggers source re-execution: the persisted output on the
dead node is gone, so the only way to recover partition p is to re-run the
source task that produced it.
SchedulingType decides whether the destination may start before the source
finishes. SEQUENTIAL (the shuffle default) means destinations wait for source
completion events; CONCURRENT lets both run together, which broadcast and
pipelined shuffle require. A CONCURRENT edge changes the scheduler's readiness
calculus for the whole vertex — see scheduler.md.
Merged inputs at the task level: GroupInputSpec
Recall TaskSpec also carries a groupInputSpecList. When several logical edges
feed a MergedLogicalInput (see
ipo-abstractions.md), the AM records
that grouping in a GroupInputSpec so the runtime knows to construct the merged
wrapper and route the constituent inputs' start() through it rather than
auto-starting them individually. This is the physical-plan counterpart to a
vertex-group union: three source vertices, three InputSpecs, one
GroupInputSpec, one reader visible to the processor.
Runtime mutation: parallelism reconfiguration
A logical Vertex declares a target parallelism. The physical parallelism
can still change — but only before the vertex schedules any task — via the
VertexManager.
grep -n "reconfigureVertex\|setVertexParallelism\|vertexReconfigurationPlanned" \
tez-api/src/main/java/org/apache/tez/dag/api/VertexManagerPluginContext.java
The modern API is reconfigureVertex, with three overloads:
// tez-api, org.apache.tez.dag.api.VertexManagerPluginContext
void reconfigureVertex(int parallelism, @Nullable VertexLocationHint locationHint,
@Nullable Map<String, EdgeProperty> sourceEdgeProperties,
@Nullable Map<String, InputSpecUpdate> rootInputSpecUpdate);
void reconfigureVertex(int parallelism, @Nullable VertexLocationHint locationHint,
@Nullable Map<String, EdgeProperty> sourceEdgeProperties);
void reconfigureVertex(@Nullable Map<String, InputSpecUpdate> rootInputSpecUpdate,
@Nullable VertexLocationHint locationHint, int parallelism);
A reconfiguration does three things atomically inside VertexImpl: resizes the
TaskImpl[], re-installs EdgeManagerPlugin instances on incoming edges (via
sourceEdgeProperties), and updates location hints. The state-machine guard
that makes it safe:
sed -n '1872,1900p' tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java
// tez-dag, org.apache.tez.dag.app.dag.impl.VertexImpl.setParallelismWrapper()
if (!tasksNotYetScheduled) {
String msg = "setParallelism cannot be called after scheduling tasks. Vertex: "
+ getLogIdentifier();
throw new TezUncheckedException(msg);
}
if (fromVertexManager && canInitVertex()) {
Preconditions.checkState(vertexToBeReconfiguredByManager,
"Vertex is fully configured but still the reconfiguration API has been called. "
+ "VertexManager must notify the framework using "
+ "context.vertexReconfigurationPlanned() before re-configuring the vertex. ...");
}
Two rules fall straight out of this code:
- Reconfiguration is illegal once any task is scheduled (
!tasksNotYetScheduledthrows). - You must announce intent first. A
VertexManagerthat will reconfigure a fully-defined vertex must callcontext.vertexReconfigurationPlanned()beforereconfigureVertex, or thecheckStatefires. This is a common custom-VertexManager bug.
Worked example: ShuffleVertexManager auto-parallelism
ShuffleVertexManager is the built-in that shrinks an over-provisioned reduce
vertex based on real upstream data size.
grep -n "DESIRED_TASK_INPUT_SIZE\|ENABLE_AUTO_PARALLEL\|MIN_TASK_PARALLELISM\|MIN_SRC_FRACTION\|MAX_SRC_FRACTION" \
tez-runtime-library/src/main/java/org/apache/tez/dag/library/vertexmanager/ShuffleVertexManager.java
| Config | Default |
|---|---|
tez.shuffle-vertex-manager.enable.auto-parallel | false |
tez.shuffle-vertex-manager.desired-task-input-size | 100 * MB |
tez.shuffle-vertex-manager.min-task-parallelism | 1 |
tez.shuffle-vertex-manager.min-src-fraction | 0.25 |
tez.shuffle-vertex-manager.max-src-fraction | 0.75 |
The sequence:
- Vertex
Rdeclared with parallelism 100 (a pessimistic upper bound). - Each upstream task's output
close()emits aVertexManagerEventcarrying per-partition byte counts (see ipo-abstractions.md). ShuffleVertexManager.onVertexManagerEventReceivedaccumulates the totals.- After
min-src-fractionof sources report, it estimatestarget = ceil(totalBytes / desiredTaskInputSize), clamped to[min-task-parallelism, originalParallelism]. - It calls
reconfigureVertex(target, null, updatedEdgeProps)— say 100 → 17. VertexImplresizesTaskImpl[]and rebuilds the incomingSCATTER_GATHERedge's manager so 100-partition outputs route into 17 destinations (each destination now reads a range of the original partitions).
sequenceDiagram
participant SRC as Upstream tasks
participant VM as ShuffleVertexManager
participant V as VertexImpl (R)
participant E as Incoming Edge
SRC-->>VM: VertexManagerEvent (per-partition byte stats)
VM->>VM: accumulate; wait for min-src-fraction
VM->>V: reconfigureVertex(17, hint, edgeProps)
V->>V: resize TaskImpl[] 100 -> 17 (tasksNotYetScheduled == true)
V->>E: setEdgeProperty(new EdgeManager)
Note over E: now routes 100 partitions -> 17 dests
V->>V: schedule 17 tasks
Reading exercise
# When is the EdgeManagerPlugin instantiated / re-installed?
grep -n "createEdgeManager\|edgeManager =\|setEdgeProperty" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/Edge.java
# Factory methods on EdgeProperty and which require an EdgeManagerPluginDescriptor
grep -n "public static EdgeProperty create" \
tez-api/src/main/java/org/apache/tez/dag/api/EdgeProperty.java
# The parallelism guard, verbatim
sed -n '1872,1900p' tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java
# Tests that pin each router's behavior
grep -rln "ScatterGatherEdgeManager\|BroadcastEdgeManager\|OneToOneEdgeManager" tez-dag/src/test
Answer:
- For
SCATTER_GATHER,A(3) → B(2): how many partitions does eachAtask emit, and how many physical inputs does eachBtask have? Cite the twogetNum...methods. - For
ONE_TO_ONE, what exactly throws if the upstream auto-parallelizes 100 → 17 after the destination is initialized? Quote the precondition. - For
BROADCAST, express the data amplification in terms of source and destination parallelism. - Which
EdgeManagerPluginmethods are called once per edge, and which per destination-task init? UseEdgeManagerPluginOnDemand.prepareForRoutingvsrouteDataMovementEventToDestinationto answer. - Trace where an
InputSpec'sphysicalEdgeCountcomes from, fromgetNumDestinationTaskPhysicalInputsto the constructor of yourAbstractLogicalInput.
Then answer in prose:
- Why must the destination vertex's parallelism be resolved before any event
routes to it? Cite the
numDestTasks < 0branch inEdge.routingToBegin. - What must a custom
VertexManagercall beforereconfigureVertexon a fully-defined vertex, and what happens if it forgets?
Common bugs and symptoms
| Symptom | Likely cause |
|---|---|
setParallelism cannot be called after scheduling tasks | reconfigureVertex invoked after scheduleTasks; fix VertexManager ordering |
Vertex is fully configured but still the reconfiguration API has been called | forgot context.vertexReconfigurationPlanned() before reconfiguring |
1-1 source and destination task counts must match | auto-parallelism broke a ONE_TO_ONE invariant; disable it on that edge |
Not expected to route events ... until parallelism is determined | routing began while destination parallelism was still -1; VertexManager never set it |
Destination task receives 0 DataMovementEvents | custom edge manager returned 0 from getNumDestinationTaskPhysicalInputs |
| Wrong row counts after a custom join | CUSTOM edge manager mis-routed partitions; fence-post bug in the routing metadata |
BROADCAST edge OOMs the destination | srcParallelism × payload exceeds destination heap; shrink the broadcast side or switch to PERSISTED and stream from disk |
Validation: prove you understand this
- Given
A(parallelism 4) SCATTER_GATHER→ B(parallelism 3), compute the number ofDataMovementEvents thatB.1receives. Show the arithmetic from the twogetNum...methods. - In one sentence each: when is an
EdgeManagerPluginre-instantiated, and when does it survive a reconfiguration? CiteEdge.setEdgeProperty. - Write a one-paragraph rejection of "let's just use
BROADCASTfor our 500-task lookup vertex," citing the concrete amplification factor. - Quote the exact line in
VertexImpl.setParallelismWrapper(from your owngrep -n, not a memorized number) that rejects reconfiguration after scheduling. - Sketch a
CUSTOMEdgeManagerPluginfor a range-partitioned merge where source taskiemits keys in[i*R, (i+1)*R)and the destination isKtasks withKpossibly ≠ source parallelism. DefinegetNumDestinationTaskPhysicalInputs,getNumSourceTaskPhysicalOutputs, and the on-demand routing rule in code. - Trace one
VertexManagerEventfrom an upstreamclose()(see ipo-abstractions.md) to areconfigureVertexcall, naming every class it passes through.