Repository navigation
[PyBindings] Torch-free follow-ups: reused inputs, error reasons, bfloat16 shape, dtype names, tests, docs - #24146
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24146
Note: Links to docs will display an error until the docs builds have been completed. ✅ No FailuresAs of commit 124aef2 with merge base 3a37515 ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
@claude review |
|
Claude finished @shoumikhin's task in 5m 12s —— View job Two behaviour changes need discussion before merge: Detailed reviewPR Review: #24146SummaryThe PR stops reruns from reading overwritten planned inputs, adds the runtime's logged error lines to exceptions, keeps the shape of bfloat16 results in the buffer view, and reports dtypes by name. Two changes need discussion: the Backward Compatibility
Code Quality
Testing
RecommendationNeeds Discussion: decide whether |
b54db82 to
5d673c2
Compare
|
Thanks for the review. Changes and answers:
|
|
@claude review |
|
Claude finished @shoumikhin's task in 6m 7s —— View job The fix now gives right numbers on reruns. But a rerun after Detailed reviewPR Review: #24146SummaryThe PR installs memory-planned inputs again before reruns, attaches the runtime's logged errors to exceptions, keeps the shape of bfloat16 results in the buffer view, and reports dtypes by name. The two items under Backward Compatibility change what existing callers get; the rest are smaller design and test gaps. Points already settled in the author's reply ( Backward Compatibility
API Design
Code Quality
Testing
RecommendationNeeds Discussion: settle one rule for when a rerun reads the caller's inputs, and decide whether the integer buffer view should apply to dtypes other than bfloat16 (along with the round-trip change). Neither needs a large code change, but both change behaviour that users will notice. Not reviewed in depth: |
5d673c2 to
c7d07b8
Compare
|
Thanks. Second round:
|
|
@claude review |
|
Claude finished @shoumikhin's task in 4m 59s —— View job Nothing here blocks the merge. One new correctness gap remains, and only in the ATen build: an input whose shape is changed in place passes the storage check and is installed again with that new shape. Two test points from round 2 also still have no answer. Detailed reviewPR Review: #24146SummaryThe PR installs memory-planned inputs again at every Code Quality
Testing
Performance
RecommendationApprove once the ATen shape check is fixed or explicitly scoped out, since the ATen build was not tested. The other points are minor. Not reviewed in depth: the strategies in |
|
Thanks. On the confirmation round:
|
|
@claude review |
|
Claude finished @shoumikhin's task in 7m 3s —— View job Nothing blocks the merge. One new case should be documented or handled first: a Detailed reviewPR Review: #24146SummaryThe PR installs memory-planned inputs again on every run, adds the runtime's logged error lines to exceptions, keeps the shape of results whose dtype has no buffer format, and reports dtypes by name. This round found one new behaviour change (borrowed results passed back in), a storage check that skips DLPack inputs, a docstring error, and test gaps that are still open. I left out points the author already settled or scoped out: the ATen shape check, the "failed partway" message, UTF-8 truncation, qint formats, the double copy after The diff was taken against Code Quality
API Design
Testing
RecommendationApprove, once the borrowed-result case is documented or handled. It is the only point here that can give wrong numbers silently. The other points are minor and can follow. |
c229661 to
f159467
Compare
|
@claude Is there a BC breaking API change and violating https://docs.pytorch.org/executorch/stable/api-life-cycle.html? |
|
Claude finished @mergennachin's task in 1m 27s —— View job No, this PR doesn't break the API life cycle policy. Every API whose behaviour changes is in DetailsWhat the policy saysFrom
How the changed APIs are marked on
|
| Change | API | State | Breaking? |
|---|---|---|---|
ExecuTorchResult.dtype gives a str instead of np.dtype |
ExecuTorchResult |
Experimental | Yes, and allowed |
ExecuTorchResult.dtype_name removed |
ExecuTorchResult |
Experimental | Yes, and allowed |
| Buffer/NumPy view of bfloat16, float8, qint and similar dtypes is now an integer array in the real shape, where before it was flat bytes or raised | ExecuTorchResult |
Experimental | Yes, and allowed |
ExecuTorchMethod.execute() installs planned inputs again on every run; a kept-input Module call does the same |
ExecuTorchMethod / ExecuTorchModule |
Experimental | Changes behaviour, and allowed |
TensorInfo.dtype_name() added |
TensorInfo |
Experimental | No, it only adds |
TensorInfo.__repr__ prints dtype=float32 instead of dtype=Float |
TensorInfo |
Experimental | A repr string isn't an API contract |
| Exceptions now include the error lines the runtime logged | all bindings | n/a | Same exception types; only the message text changes |
The stable path: executorch.runtime.Method.execute
- It calls
self._method(inputs, clone_outputs=...), so it always goes through__call__with a full input list. __call__installed every input on every call before this PR as well, and it skips the extra copy the PR adds (inputs_just_installed). So the rerun change can't reach this path, and the numbers it returns are the same.- With PyTorch installed, the outputs are still
torch.Tensor, so none of theExecuTorchResultchanges show up. - Without PyTorch, the outputs are
ExecuTorchResult, so a stable call can hand back a changed.dtype. The returned class is the experimental one, though, so the policy still allows this. The PR only rewrites the docstring ofMethod.execute; the signature stays the same.
Caveats
- Release status of
ExecuTorchResultnot confirmed. The author says neitherExecuTorchResultnordtype_namehas shipped in a release.version.txtonmainsays1.6.0. I couldn't check therelease/*branches: the checkout is shallow, and the GitHub API and web fetches weren't allowed in this job. Even if it did ship, it is marked experimental, so the policy is still met. - Release notes. The PR has the
release notes: apilabel. It would help users to list these there as experimental API changes (ExecuTorchResult.dtype→str,dtype_nameremoved, the integer buffer view, inputs installed again onexecute()), even though the policy doesn't require a deprecation. - Possible follow-up. If
ExecuTorchResultis meant to become the stable torch-free output type, it may be worth settling itsdtypetype before the experimental marking comes off. Once the class is stable, changing it would need a deprecation.
I only read the code: docs/source/api-life-cycle.md, pybindings.pyi and runtime/__init__.py on main (cf6d4f2), and the PR diff. Nothing was built or run.
| from executorch.runtime import Runtime | ||
|
|
||
| method = Runtime.get().load_program("model.pte").load_method("forward") | ||
| out = method.execute([np.ones((1, 3, 224, 224), np.float32)])[0] |
There was a problem hiding this comment.
why are we supporting numpy now?
this looks like a new feature as opposed to fixing bugs.
also for normal torch, we do torch.from_numpy(np_array) and pass in torch.tensor type
There was a problem hiding this comment.
okay claude answered it for me.
can you make the document read like numpy is a good fallback option if you don't have torch installed? having the numpy path as the default sample code is weird.
I would assume that most users will have torch installed and want a simple way, and we can have a separate fallback option for numpy.
There was a problem hiding this comment.
Done. The torch path is now the main example: torch tensors in, torch tensors out. NumPy moved to a short "Without PyTorch" section at the end. It is the fallback for users who do not have PyTorch, and it also shows DLPack (for example CuPy) for GPU data. The Method.execute docstring follows the same order.
|
@claude Did the pybinding API support numpy before and if not, why are we adding now? WHat's the behavior for normal torch library? Should we strive for consistency as opposed to adding new feature? |
|
Claude finished @mergennachin's task in 1m 2s —— View job This PR doesn't add NumPy support: DetailsDid the pybindings accept NumPy before this PR?Yes. On
So the change you commented on ( Why the bindings take NumPy at allThe portable bindings can be built and run without PyTorch ( How PyTorch behaves
Consistency or a new feature?My recommendation:
I can push that doc edit if you and @shoumikhin want it. I only read the code on |
08572f5 to
8cd7b33
Compare
|
@claude review |
|
Claude finished @shoumikhin's task in 5m 7s —— View job Nothing blocks the merge, and this round found nothing new that affects correctness. Since Detailed reviewPR Review: #24146SummaryThe PR installs memory-planned inputs again at every run, attaches the runtime's logged error lines to exceptions, keeps the shape of results whose dtype has no buffer format, reports dtypes by name, and makes torch the main path in the docs with NumPy as the fallback. Three reviewers went through
The torch-first docs that @mergennachin asked for read correctly against the code: the example APIs and the Not covered in depth: the strategies in Performance
RecommendationApprove. The one point above is a documentation follow-up, and the author has already listed the other follow-ups. |
We are adding support for our pybindings without Torch for use in robotics applications where they can drop the Torch dependency. Existing users should see no change in behavior if you were using torch before everything will continue as normal, but for torchless users this lets them have io without bringing on a heavy aten dep if they dont need it. |
|
@shoumikhin we have added too many test and test files here that are all very similar to one another please trim it down before landing. This is one of the areas we really need to push back against ai spam. |
|
Agreed on the memory note. Each |
By default, export puts each input in planned memory and lets later
values reuse that memory once the input is no longer needed. A method
that writes an input in place also changes that memory. So after a run,
the input bytes may hold other data. Running again without setting the
inputs read that data and returned wrong numbers with no error:
method.set_inputs([x, y]); method.execute() # right
method.execute() # wrong, silently
The same happened with Module.plan_execute() and with a partial input
list, which reuse inputs kept from an earlier call.
The bindings already hold the inputs the caller passed, until they are
replaced. Now Method installs its memory-planned inputs again from what
it holds at every execute(), except when one call sets the inputs and
runs at once, and Module does the same for kept inputs a call omits. What
they hold is the caller's tensor or buffer, or the copy the bindings
made of it when it needed converting.
This changes three things on purpose:
- An input is read at each run, so an in-place change the caller made
to it since it was passed is now seen, as it already was for inputs
that are not memory planned.
- For the same reason, a change to the storage of a torch tensor or
buffer input is refused with the "pass it again" error those inputs
already get.
- A method that writes an input in place, such as state.add_(x), used
to carry that write into the next run that reused the input, as
repeated Method::execute() calls in C++ still do. Now that run starts
from the input as passed, the same as a call that passes it again.
When the runtime refuses an input or fails to load a method, it writes
the reason to its log and returns only an error code. The Python
exception then showed just the code, often with a typo:
RuntimeError: method->set_inputs() for method 'forward' failed with error 0x10
RuntimeError: Failed to load method forward, error: 0x:22
The reason was already printed on stderr (for example "Expected shape
(2, 2), but received (3, 2)", or that libgomp.so.1 could not be found
when loading a CUDA method without PyTorch), but not in the exception,
so it was easy to miss and impossible to catch.
The bindings already forward runtime logs to Python. They now also keep
the error lines logged on the calling thread during the current call and
add them to the exception:
RuntimeError: method->set_inputs() for method 'forward' failed with
error 0x10: Attempted to resize a static tensor. Expected shape
(2, 2), but received (3, 2).; Error resizing tensor at input 0
This also fixes the "0x:" typo in eight messages.
NumPy and the Python buffer protocol have no bfloat16 type. So far a
bfloat16 result was exported as a flat run of bytes: a (2, 3) result
showed up as 12 bytes, so a reader got a different shape with no error,
and lost the layout of a result that is not contiguous. Other dtypes
without a buffer format, such as float8 or the quantized types, could
not be read through the buffer protocol at all.
Now such a dtype is shown as an integer of the same width, in the
result's real shape and strides: unsigned in general, and signed for
qint8 and qint32, whose storage is signed. A (2, 3) bfloat16 result reads
as a (2, 3) array of uint16, with each element in the right place; only
the meaning of each element needs reinterpreting. DLPack still carries
the real bfloat16 dtype.
out = method.execute([x])[0] # a (2, 3) bfloat16 result
memoryview(out).format # before "B", after "H"
memoryview(out).shape # before (12,), after (2, 3)
The Python bindings named a tensor's data type in three ways.
ExecuTorchResult.dtype was a NumPy dtype and raised for types NumPy
lacks, such as bfloat16, which is why ExecuTorchResult.dtype_name, a
torch-style name, was added beside it. TensorInfo.dtype() was the
ScalarType integer, and TensorInfo's repr printed the C++ name
("Float").
Now the result and the method metadata report a dtype by its name, the
way torch spells it: "float32", "bfloat16", "int64". The names come from
the one table the bindings already use to read inputs, so every
ExecuTorch dtype has one, with or without torch installed. For every
dtype NumPy has, the name is also NumPy's.
method = program.load_method("forward")
out = method.execute([x])[0]
out.dtype # "bfloat16"
method.metadata.output_tensor_meta(0).dtype_name() # "bfloat16"
getattr(torch, out.dtype) # torch.bfloat16
- ExecuTorchResult.dtype returns the name and never raises.
- ExecuTorchResult.dtype_name is removed. It was experimental and never
in a release, and dtype now gives the same name.
- TensorInfo.dtype_name() is new and returns the same name.
TensorInfo.dtype() still returns the integer, since released code
depends on it.
- TensorInfo's repr prints dtype=float32, like ExecuTorchResult's.
- ExecuTorchResult.__array__() now builds its array from the buffer view,
so for a dtype NumPy lacks it returns the same integer view as the
buffer, where it used to raise. np.asarray() already read the buffer.
Error messages about input dtypes keep their existing wording.
The runtime API reference showed only torch.Tensor inputs and outputs, and its note on running without PyTorch had no example. Most users have PyTorch installed, so the torch path stays the main example. A short "Without PyTorch" section now shows the fallback: pass NumPy arrays and read the ExecuTorchResult outputs with NumPy, or use DLPack, for example with CuPy, for data on a GPU. The Method.execute docstring says the same: inputs and outputs are typically torch.Tensor objects, and without PyTorch it takes NumPy arrays, buffers, DLPack objects or ExecuTorchResult values and returns ExecuTorchResult values. The reference also lists ExecuTorchResult.device instead of the removed dtype_name.
8cd7b33 to
124aef2
Compare
|
Thanks, trimmed: new test files 2 to 0, new tests 7 to 2, added test lines about 730 to 72. The leak and fuzz tests are gone. One test checks that inputs are read again on every run. One checks that errors say why a call was refused. The rest are small edits to existing bfloat16 and dtype tests. |
|
@claude review |
|
Claude finished @shoumikhin's task in 4m 44s —— View job Nothing blocks the merge, and the trimmed diff adds no correctness problems. Two minor points are new: a comment that wrongly says planned and non-planned inputs now behave the same, and a Detailed reviewPR Review: #24146SummaryThe PR installs memory-planned inputs again at every run, adds the runtime's logged error lines to exceptions, keeps the shape of results whose dtype has no buffer format, reports dtypes by name, and makes torch the main path in the docs. Three reviewers read all 9 changed files in Code Quality
RecommendationApprove. Both points are minor and can go with the follow-ups you already listed. |
|
Thanks. Both points are fair, and both go into the follow-ups I listed before, not this PR. The reinstall comment gets reworded, and the README rerun section gets one line: a planned input that the program writes in place starts again from what the caller passed on every run, while an input that is not planned keeps the write. The |
|
@pytorchbot cherry-pick --onto release/1.6 -c fixnewfeature |
Cherry picking #24146The cherry pick PR is at #24209 and it is recommended to link a fixnewfeature cherry pick PR with an issue. Details for Dev Infra teamRaised by workflow job |
1. Stop reusing overwritten inputs
Problem. Export lets later values reuse an input's planned memory, so a second run without new inputs gave wrong numbers silently:
So did
Module.plan_execute()and partial input lists.Change.
Methodinstalls memory-planned inputs again from what the caller passed at everyexecute();Moduledoes the same for kept inputs a call omits. So inputs are read at each run: in-place edits are seen, a storage change raises "pass it again", and an input the method writes in place (state.add_(x)) no longer carries over, unlike repeatedMethod::execute()in C++. The README notes the limits for DLPack producers andclone_outputs=Falseresults passed back in.2. Say why a call was refused
The runtime logs why it refused a call but returns only a code, so exceptions said just
error 0x10orerror: 0x22. Now they include the error lines the runtime logged during the call:3. Keep the shape of bfloat16 results
A (2, 3) bfloat16 result showed up in the buffer protocol as 12 flat bytes. Now dtypes without a buffer format show as same-width integers in the real shape; DLPack still carries the real dtype. Pass a bfloat16 result back in as the result or its bytes, not
np.asarray(result).4. Report a dtype by its name
ExecuTorchResult.dtypenow returns a string such as"bfloat16"instead of a NumPy dtype (which raised for bfloat16), and the newTensorInfo.dtype_name()returns the same name. The experimentaldtype_nameproperty is removed.TensorInfo.dtype()still returns the integer; its repr printsdtype=float32.5. Docs
The API reference keeps torch tensors in and out as the main example. A short "Without PyTorch" section shows the fallback: NumPy arrays in,
ExecuTorchResultvalues out, and DLPack (for example CuPy) for GPU data. TheMethod.executedocstring follows the same order.Testing
On Linux x86_64 with an NVIDIA GPU:
test_pybindings.py(inputs installed again at each run, and the reason in error messages), plus small edits to existing tests for the bfloat16 buffer view and dtype names. Both new tests, and most of the edited ones, fail on the base branch; all of them pass here.