On the ambiguity of `.shape` behavior
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 25/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Estancado
- Stack tecnológico
- numpy, python
- Área
- api, backend-api-design
Línea de trabajo
Empieza leyendo los requisitos de shape en #97 y la discusión relacionada en #839; después, compáralos con los ejemplos de PyTensor de este issue. Se considera terminado cuando exista una decisión documentada sobre la semántica de shapes dinámicos frente a estáticos, los nombres de atributos propuestos y si library.shape(x) sigue la forma dinámica o la estática.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
According to #97 a library can decide to either return a tuple[int | None, ...] or a tuple-like object that:
The returned value should be a tuple; however, where warranted, an array library may choose to return a custom shape object. If an array library returns a custom shape object, the object must be immutable, must support indexing for dimension retrieval, and must behave similarly to a tuple.
This seems like a recipe for disaster? The second option allows to operate on shape graphs, whereas the first would fail when you try to act on None, say to find the size of some dimensions by doing prod(x.shape[1:]) (forced example so that .size wouldn't be applicable).
In PyTensor we have the distinction between variable.shape and variable.type.shape, that correspond to those two kinds of output. They are flipped though, and it seems odd to make variable.shape return a tuple with None. It doesn't make sense to build a computation on top of static shape, because those None are not linked to anything.
import numpy as np
import pytensor
import pytensor.tensor as pt
x = pt.tensor("x", shape=(3, None,))
print(x.shape) # Shape.0
print(x.type.shape) # (3, None)
# Could not possibly work with x.type.shape
out = pt.broadcast_to(x, (2, x.shape[0], x.shape[1]))
print(out.type.shape) # (2, 3, None)
assert out.eval({x: np.ones((3, 4))}).shape == (2, 3, 4)
assert out.eval({x: np.ones((3, 5))}).shape == (2, 3, 5)
Besides that, we sometimes also allow users to replace variables with different static shapes, although it's arguable a bit of an undefined behavior. It seems to contradict the specification that it must be immutable, so happy to say it's out of scope:
new_x = pt.tensor("x", shape=(4, 4))
# Even ignoring the issue of using None for unknown dimensions, the following could not work
# if the original graph was built on top of the static 3 dim length, as that's not "connected" to anything.
new_out = pytensor.graph.clone_replace(out, {x: new_x}, rebuild_strict=False)
print(new_out.type.shape) # (2, 4, 4)
assert new_out.eval({new_x: np.ones((4, 4))}).shape == (2, 4, 4)
Proposal
Would make sense to separate the two kinds of shape clearly? Perhaps as variable.shape and variable.static_shape. The first should be valid to build computations on top of variable shapes, statically known or not, while the second would allow libraries to reason as much as possible about what is known (and choose to fail if the provided information is insufficient) without having to try and probe which kind of shape output is returned by a specific library.
This is somewhat related to #839, where a library may need as much information as possible to make a decision. Perhaps a static_value would also make sense for a library to return the entries that can be known ahead of time. Anyway that should be discussed there.
If both options make sense, I would argue that .shape should behave like pytensor does.
The standard should also specify if library.shape(x) should match x.shape or x.static_shape. Again I think it should match the first.
- Lenguaje dominante
- Python
- Estrellas
- 281
- Forks
- 52
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de data-apis/array-api
-
Dificultad 1/5 1-3 horas Aptitud para principiantes 88/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
-
Maintenance
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
Todos los issues de data-apis/array-api
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100