llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

llm_runner: add streaming text boundary helpers - #20242

Merged
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers
Jun 15, 2026
Merged

llm_runner: add streaming text boundary helpers#20242
mergennachin merged 1 commit into
mainfrom
llm-pr-b-runner-streaming-helpers

Conversation

@mergennachin

@mergennachinmergennachin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.

This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.

The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.

#20001

CopilotAI review requested due to automatic review settings June 12, 2026 20:12
@pytorch-bot

pytorch-botBot commented Jun 12, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 6 Pending

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

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

@meta-clameta-claBot 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 Jun 12, 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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runner-level helper utilities to make incremental text streaming safer by (1) avoiding emission of incomplete UTF-8 sequences and (2) avoiding emission of prefixes that could still be part of a stop-string match across chunk boundaries. This supports the broader goal of making ExecuTorch’s LLM serving stack streaming-correct without duplicating fragile string logic across server paths.

Changes:

  • Introduces utf8_complete_prefix_len() to compute a UTF-8-safe emit prefix length.
  • Introduces stop_safe_prefix_len() to compute a stop-string-safe emit prefix length (with UTF-8 boundary snapping) and a stop_hit signal.
  • Adds focused unit tests for both helpers.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

FileDescription
extension/llm/runner/util.hAdds UTF-8 boundary and stop-string boundary helper functions for streaming-safe text emission.
extension/llm/runner/test/test_util.cppAdds unit tests covering UTF-8 prefix handling and stop-string-safe prefix handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/llm/runner/util.h Outdated
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Review this PR

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 2m 15s —— View job


Review: llm_runner: add streaming text boundary helpers

  • Read changed files (util.h, test_util.cpp)
  • Analyze helper correctness & edge cases
  • Check whether prior Copilot feedback is addressed
  • Post review

Nice, focused change. The helpers are pure, well-documented, and the test coverage hits the important boundary cases. I read this as good to merge modulo the minor notes below.

Prior Copilot feedback — addressed ✅

Both earlier comments referred to an out-of-bounds text[end] access and a missing hold == 0 test. The current revision resolves both:

  • Snapping is now delegated to a dedicated utf8_safe_prefix_len() (util.h:117) that backs up over continuation bytes and validates the lead byte's expected length, rather than indexing text[end]. No exclusive-offset read remains.
  • SingleByteStopMissEmitsEverything (test_util.cpp:96, stops={"Z"}hold == 0) and HoldZeroDoesNotEmitDanglingUtf8LeadByte (test_util.cpp:138) cover the no-hold-back path, including the dangling-lead-byte case.

Correctness

The three functions look correct:

  • utf8_complete_prefix_len — forward scan, invalid lead byte treated as len 1 (emit-and-replace) so output never stalls. Good.
  • utf8_safe_prefix_len — backward snap, returns len only when a complete char ends exactly at len, else lead_pos. Never exceeds the requested length. Good.
  • stop_safe_prefix_len — earliest-occurrence wins, conservative max_len - 1 hold-back, snapped down to a UTF-8 boundary. The text.size() <= hold → 0 guard is correct.

Minor observations (non-blocking)

  1. Stop offset isn't UTF-8-snapped (util.h:182-185). When a stop is found, earliest is returned directly. This is correct given the documented precondition (input is complete-UTF-8 and stops are real text). But a stop that is a bare continuation byte (e.g. "\x82") could split a multi-byte char. Worth a one-line comment noting the snap is intentionally skipped because the precondition makes a mid-char match impossible — or a defensive utf8_safe_prefix_len(text, earliest) if you want belt-and-suspenders.

  2. Hold-back is coarser than necessary (util.h:186). Holding back max_len - 1 bytes unconditionally is always safe, but it holds back bytes even when the trailing text can't be a prefix of any stop (e.g. "hi ST" with stop "STOP" holds back " ST", not just "ST"). This only adds streaming latency, never incorrectness — fine to leave, but a comment clarifying the conservative choice would help future readers.

  3. Minor duplication.utf8_complete_prefix_len and utf8_safe_prefix_len each carry their own lead-byte length table (forward vs. backward). Two uses is borderline per the repo's "no trivial single-use helpers" guidance, so I'd leave it — just flagging that the two tables must stay in sync.

Test suggestions (optional)

  • A stop that straddles into a held-back multi-byte tail, e.g. text ending in a partial multi-byte char where hold lands mid-character — confirms the snap interacts correctly with hold-back beyond the existing HoldBackSnapsToUtf8Boundary case.
  • Multiple stops of differing lengths where the longer one drives max_len but the shorter one is the earliest match — confirms earliest/max_len are tracked independently.
    · branch llm-pr-b-runner-streaming-helpers

The serving stack streams decoded text incrementally, but token boundaries do not guarantee user-visible text boundaries. A chunk can end in the middle of a UTF-8 sequence, or immediately before bytes that complete a configured stop string. Handling that ad hoc in each server path would make streaming correctness easy to regress.
This adds small, model-agnostic runner helpers for computing UTF-8-safe and stop-string-safe prefixes before emitting text. Keeping the logic in the runner utility layer gives both generic and model-specific workers one tested implementation instead of duplicating fragile string handling.
The change is intentionally narrow: it introduces pure helper functions and focused unit coverage only. It does not change runner generation behavior on its own.
CopilotAI review requested due to automatic review settings June 12, 2026 21:03
@mergennachin
mergennachinforce-pushed the llm-pr-b-runner-streaming-helpers branch from f0bc5c8 to ee1ea74CompareJune 12, 2026 21:03

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/util.h
Comment threadextension/llm/runner/test/test_util.cpp
@mergennachin

Copy link
Copy Markdown
ContributorAuthor

@claude Validate the last copilot comments, wdyt?

@claude

claudeBot commented Jun 12, 2026

Copy link
Copy Markdown

Claude finished @mergennachin's task in 1m 15s —— View job


Validating the latest Copilot comments

I read util.h and test_util.cpp at the current revision. All three of the latest Copilot comments are factually correct, but they're documentation/edge-case nits rather than real bugs. My take on each:

1. util.h:108 — invalid lead bytes 0xC0/0xC1 and 0xF5–0xF7 treated as multi-byte ✅ correct, low impact

The bit tests are the "simple" UTF-8 length classification:

  • (c >> 5) == 0x60xC0–0xDF (len 2), which includes0xC0/0xC1 (only ever produce overlong encodings → always invalid).
  • (c >> 3) == 0x1E0xF0–0xF7 (len 4), which includes0xF5–0xF7 (would encode > U+10FFFF → always invalid).

So the comment "An invalid lead byte counts as length 1" is only true for continuation bytes (0x80–0xBF) and 0xF8–0xFF; the four byte values above are instead consumed as multi-byte starts. Copilot is right that this contradicts the stated contract.

Real-world impact is negligible — a sane byte-level tokenizer won't emit these — but there is a theoretical streaming stall: utf8_complete_prefix_len("\xc0") returns 0 (holds the byte waiting for a 2nd that may never be part of the same char) instead of 1. Since the whole point of the helper is "emit-and-replace rather than stall," I'd lean toward making the code match the comment. Tightening the classification is cheap:

} elseif (c >= 0xC2 && c <= 0xDF) { // exclude 0xC0/0xC1
len = 2;
} elseif ((c >> 4) == 0xE) {
len = 3;
} elseif (c >= 0xF0 && c <= 0xF4) { // exclude 0xF5-0xF7
len = 4;
} else {
len = 1;
}

Alternatively, if you'd rather keep the simple bit-twiddling, just soften the comment to say "structurally-complete lead byte" instead of "valid." Either resolves the mismatch.

2. util.h:163 — contract says complete-UTF-8 but tests feed incomplete tails ✅ correct

HoldBackWithIncompleteUtf8TailSnapsToBoundary ("ab\xe2\x82") and HoldZeroDoesNotEmitDanglingUtf8LeadByte ("ab\xc3") deliberately pass truncated trailing codepoints, and stop_safe_prefix_len handles them correctly via utf8_safe_prefix_len's back-snap. So the function is actually more robust than its documented contract. Worth a one-line tweak noting the back-snap defensively handles an incomplete trailing codepoint, so the "expected complete-UTF-8" line isn't misread as a hard precondition. Purely a doc fix.

3. test_util.cpp:88 — no coverage for invalid lead-byte ranges ✅ correct, and coupled to #1

The tests only exercise an invalid continuation byte (0x80). Whatever you decide for #1, lock it in with assertions, e.g.:

EXPECT_EQ(utf8_complete_prefix_len("\xc0"), 1u); // overlong leadEXPECT_EQ(utf8_complete_prefix_len("\xf5"), 1u); // > U+10FFFF leadEXPECT_EQ(utf8_complete_prefix_len("\xf8"), 1u); // already len 1 today

Note "\xc0" / "\xf5" will only pass with the #1 code fix; with the current code they'd return 0. So this test is the natural way to decide between "fix code" vs. "soften comment."


Bottom line: none of these are correctness/safety bugs — no OOB reads, output never stalls in practice. They're a real but minor comment-vs-code mismatch (#1), a doc clarification (#2), and the missing test that pins it down (#3). I'd resolve #1 by tightening the classification and add the #3 assertions; #2 is a one-liner. Happy to push those changes if you want — just say the word.
· branch llm-pr-b-runner-streaming-helpers

@mergennachin
mergennachin merged commit 06143cb into mainJun 15, 2026
187 checks passed
@mergennachin
mergennachin deleted the llm-pr-b-runner-streaming-helpers branch June 15, 2026 14:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis 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

@mergennachin@Gasoonjia