Skip to content

Treat an empty LLM data path as absent - #21835

Merged
JakeStevens merged 1 commit into
pytorch:mainfrom
john-rocky:llm-empty-data-path
Aug 14, 2026
Merged

Treat an empty LLM data path as absent#21835
JakeStevens merged 1 commit into
pytorch:mainfrom
john-rocky:llm-empty-data-path

Conversation

@john-rocky

Copy link
Copy Markdown
Contributor

LlmModuleConfig.Builder defaults dataPath to "", and create_text_llm_runner takes whatever the optional holds — the empty string included. So an Android caller that never asked for a separate data file still reaches the loader with "":

[llm_runner_helper.cpp:255] Reading metadata from model
[mmap_data_loader.cpp:94] Failed to open : No such file or directory (2)
[llm_runner_helper.cpp:105] Failed reading method names
[llm_runner_helper.cpp:258] Failed to get metadata from model

Because LlmModule.load() reports failure through throwExecutorchException, this comes out as Fatal signal 6 (SIGABRT) with abort message 'ptr' and the process dies, rather than an error the app can catch. That is what makes it hard to place: nothing in the message points at a data path, and the same .pte and tokenizer work everywhere else.

Reproducing it takes only the documented builder call — no data file, no unusual model:

LlmModule(
    LlmModuleConfig.create()
        .modulePath(model)
        .tokenizerPath(tokenizer)
        .modelType(LlmModuleConfig.MODEL_TYPE_TEXT)
        .build())
    .load()

Passing .dataPath(null) explicitly is the workaround.

The change

The builder default becomes null, and both runner factories ignore an empty path so a caller in any language whose "no value" is an empty string lands on the no-data-file branch rather than a failed mmap. Three files: the Kotlin default, the two data_path.has_value() checks, and a test for the default.

I kept the guard in C++ as well as fixing the Kotlin default, because the optional is public API and an empty string will keep arriving from bindings that have no null.

Verification

On a Pixel 8a, arm64, LFM2.5-350M exported to XNNPACK 8da4w:

  • before: abort during load(), every time
  • after: loads in ~2.0 s and generates, prefill 23.8 tok/s / decode 11.9 tok/s

Those rates are low because the phone was locked while measuring, which parks the app on the little cluster — not a property of this change.

I also confirmed the failure is not the model or the tokenizer: the .pte exposes the full metadata method set the runner expects, and the tokenizer loads and encodes identically on the same device through a standalone harness.

This PR was authored with Claude.

LlmModuleConfig's builder defaulted dataPath to "", and create_text_llm_runner
takes any value the optional holds -- including the empty string -- so an
Android caller that never asked for a data file still reached the loader with
"". The load then failed on `Failed to open : No such file or directory`, and
because LlmModule.load() surfaces that through a native exception, the process
aborted rather than returning the error.

The builder default is now null, and both runner factories ignore an empty
path, so a caller in any language whose "no value" is an empty string lands on
the no-data-file branch instead of a failed mmap.
@pytorch-bot

pytorch-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21835

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit fd3ef18 with merge base abc5586 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 14, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@JakeStevens

Copy link
Copy Markdown
Contributor

I'm curious overall, how was your experience exporting and running the lfm2.5 family?

@JakeStevens
JakeStevens merged commit 88f0c74 into pytorch:main Aug 14, 2026
190 of 192 checks passed
@john-rocky

Copy link
Copy Markdown
Contributor Author

Thanks for merging it, and for asking — the export side was genuinely smooth, so the honest answer is that everything that cost me time was downstream of it.

export_llm handled the family without complaint: 350M and 1.2B, 8da4w with 8-bit embeddings, XNNPACK, straight from a config. No wrapper surgery, no op gaps. On a Pixel 8a the 350M loads in 1.0 s and the 1.2B runs, and on an iPhone 17 Pro the 350M reaches about 180 tok/s decode.

Three things did cost time, in the order I hit them.

The empty dataPath you just merged was the first, and it was expensive out of proportion to its size — the process aborts with Abort message: 'ptr' and nothing in the output mentions a path, so I went through the model and the tokenizer before suspecting the config.

The second was the tokenizer. LFM2.5's tokenizer.json carries \s+(?!\S), RE2 rejects the lookahead, and the PCRE2 fallback does pick it up and encode correctly — but on iOS, where the prebuilt SwiftPM package has no regex_lookahead product, the same file takes the process down with signal 6. Patching the pattern to \s+$ is a workaround; a clearer failure than an abort would have saved a day.

The third I could not close, and it is the one I would most like a second opinion on: generation degenerates through TextLLMRunner while the same .pte answers normally when stepped one token at a time with an explicit input_pos. Filed with the isolation in #21858 — it reproduces on macOS with the pip wheel, it survives rebuilding the model with and without custom SDPA and with static and dynamic shapes, and Qwen3.5-0.8B through the same runner is fine, so it looks like an interaction with LFM2.5's hybrid conv/attention rather than anything about the export. I am happy to run experiments against it or test a patch.

One smaller note while I have your attention: extension/image has been unreachable from Android since it landed — it is not built into the AAR at all. #21830 adds the JNI and Kotlin bindings, verified on a Pixel 8a. It is waiting on a first-time-contributor CI approval rather than on review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants