Compute tf.gather's output shape instead of passing the table through (#727, fixing #815) made tf.gather return ⊤ whenever the indices' shape is unknown, discarding the trailing axes of the table, which are known. Shapes that resolved at 0.52.77 are unknown-rank at 0.52.78.
The Code Path
Gather extends EmbeddingLookup, whose getDefaultShapes derives ids.shape + params.shape[1:]:
if (paramsShapes == null || idsShapes == null) return null;
An unknown ids therefore poisons the whole result, including params.shape[1:], which does not depend on ids at all.
The Pattern
table = tf.zeros((100, 16))
idx = something_the_analysis_cannot_resolve() # rank unknown
picked = tf.gather(table, idx) # truth: idx.shape + (16,)
Before #727 the model passed the table through, so picked reported the table's own shape, rank 2 with a known trailing axis. After it, picked is ⊤.
What Makes This Awkward
The pass-through was not right in general: it named the table's leading extent where the truth is the indices' extent. It happened to read correctly whenever the table's leading axis was itself unresolved, since both then print the same way. So the previous answer was accidentally right and the new one is honestly unknown, which is not the usual regression shape.
That rules out the obvious repair. Assuming ids is rank 1, which it usually is, would under-rank every call that passes a higher-rank index tensor, and an argument of rank 3 is no more a subtype of a rank-2 specification than a rank-1 one is. The reasoning #813 used against over-ranking applies in both directions.
Two Directions That Are Sound
Resolve the indices. The common idiom builds them by slicing a column out of an edge list, idx = pairs[:, 0], which is exactly what #813's machinery types as rank 1. When that resolves, gather yields the correct shape on its own terms rather than by accident, which is better than what the pass-through gave.
Stop the whole-set poisoning. EmbeddingLookup is on the legacy null-means-⊤ path, and its record counterpart is ShapeResult.fromLegacy(getDefaultShapes(...)), so it inherits the all-or-nothing behavior that ShapeResult (#718) exists to replace. If ids resolves to some members and not others, crossing the resolvable ones and carrying the rest as the unknown remainder would preserve what is known. Whether ids is partially resolvable in the reported case is not established; that needs a FINE probe, and if ids is wholly ⊤ there is no partial to preserve and this direction buys nothing.
The first direction is the one that addresses the cause. The second is worth doing regardless, since the poisoning is a contract violation independent of this symptom.
Bisected
The behavior is absent at 0.52.77 and present at 0.52.78, 0.52.79 and 0.52.80, with every other input held fixed. It affects tf.nn.embedding_lookup on the same code path.
Compute tf.gather's output shape instead of passing the table through(#727, fixing #815) madetf.gatherreturn ⊤ whenever the indices' shape is unknown, discarding the trailing axes of the table, which are known. Shapes that resolved at 0.52.77 are unknown-rank at 0.52.78.The Code Path
GatherextendsEmbeddingLookup, whosegetDefaultShapesderivesids.shape + params.shape[1:]:An unknown
idstherefore poisons the whole result, includingparams.shape[1:], which does not depend onidsat all.The Pattern
Before #727 the model passed the table through, so
pickedreported the table's own shape, rank 2 with a known trailing axis. After it,pickedis ⊤.What Makes This Awkward
The pass-through was not right in general: it named the table's leading extent where the truth is the indices' extent. It happened to read correctly whenever the table's leading axis was itself unresolved, since both then print the same way. So the previous answer was accidentally right and the new one is honestly unknown, which is not the usual regression shape.
That rules out the obvious repair. Assuming
idsis rank 1, which it usually is, would under-rank every call that passes a higher-rank index tensor, and an argument of rank 3 is no more a subtype of a rank-2 specification than a rank-1 one is. The reasoning #813 used against over-ranking applies in both directions.Two Directions That Are Sound
Resolve the indices. The common idiom builds them by slicing a column out of an edge list,
idx = pairs[:, 0], which is exactly what #813's machinery types as rank 1. When that resolves,gatheryields the correct shape on its own terms rather than by accident, which is better than what the pass-through gave.Stop the whole-set poisoning.
EmbeddingLookupis on the legacynull-means-⊤ path, and its record counterpart isShapeResult.fromLegacy(getDefaultShapes(...)), so it inherits the all-or-nothing behavior thatShapeResult(#718) exists to replace. Ifidsresolves to some members and not others, crossing the resolvable ones and carrying the rest as the unknown remainder would preserve what is known. Whetheridsis partially resolvable in the reported case is not established; that needs aFINEprobe, and ifidsis wholly ⊤ there is no partial to preserve and this direction buys nothing.The first direction is the one that addresses the cause. The second is worth doing regardless, since the poisoning is a contract violation independent of this symptom.
Bisected
The behavior is absent at 0.52.77 and present at 0.52.78, 0.52.79 and 0.52.80, with every other input held fixed. It affects
tf.nn.embedding_lookupon the same code path.