Skip to content

Commit 4648639

Browse files
committed
[UPDATE] Update
[ghstack-poisoned]
1 parent 170f01d commit 4648639

3 files changed

Lines changed: 88 additions & 2 deletions

File tree

extension/llm/runner/llm_session.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
// Model-agnostic Engine/Session interfaces. Model-specific execution lives in
1010
// adapters that implement these (TextLLMSession over TextLLMRunner today;
11-
// Gemma4Session etc. later); the server and pybind layer depend only on these
12-
// interfaces, never on a concrete runner.
11+
// Gemma4Session etc. later); the serving code (HTTP control plane + C++ worker
12+
// binaries) depends only on these interfaces, never on a concrete runner.
1313

1414
#pragma once
1515

extension/llm/runner/test/test_util.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace {
1818
using ::executorch::aten::ScalarType;
1919
using ::executorch::extension::make_tensor_ptr;
2020
using ::executorch::extension::llm::convert_to_bfloat16;
21+
using ::executorch::extension::llm::stop_safe_prefix_len;
2122
using ::executorch::extension::llm::utf8_complete_prefix_len;
2223

2324
class ConvertToBFloat16Test : public ::testing::Test {
@@ -86,4 +87,39 @@ TEST(Utf8CompletePrefixLenTest, HandlesAsciiAndMultiByteBoundaries) {
8687
EXPECT_EQ(utf8_complete_prefix_len("\x80"), 1u);
8788
}
8889

90+
TEST(StopSafePrefixLenTest, NoStopsEmitsEverything) {
91+
bool hit = true;
92+
EXPECT_EQ(stop_safe_prefix_len("hello world", {}, hit), 11u);
93+
EXPECT_FALSE(hit);
94+
}
95+
96+
TEST(StopSafePrefixLenTest, StopFoundReturnsEarliestOffsetAndExcludesIt) {
97+
bool hit = false;
98+
// "STOP" begins at offset 6; emit "Hello " (6 bytes), drop the stop and rest.
99+
EXPECT_EQ(stop_safe_prefix_len("Hello STOP there", {"STOP"}, hit), 6u);
100+
EXPECT_TRUE(hit);
101+
// Earliest of several wins.
102+
hit = false;
103+
EXPECT_EQ(stop_safe_prefix_len("aXbY", {"Y", "X"}, hit), 1u);
104+
EXPECT_TRUE(hit);
105+
}
106+
107+
TEST(StopSafePrefixLenTest, HoldsBackPossiblePartialStopTail) {
108+
bool hit = false;
109+
// No full stop yet, but the trailing "ST" could become "STOP": hold back
110+
// len("STOP")-1 == 3 bytes, so of "hi ST" (5 bytes) only "hi" (2) is safe.
111+
EXPECT_EQ(stop_safe_prefix_len("hi ST", {"STOP"}, hit), 2u);
112+
EXPECT_FALSE(hit);
113+
}
114+
115+
TEST(StopSafePrefixLenTest, HoldBackSnapsToUtf8Boundary) {
116+
bool hit = false;
117+
// "ab" + "€"(3 bytes). Stop "XX" => hold back 1 byte, which would land inside
118+
// the euro sign; snap down so the multi-byte char isn't split.
119+
const std::string text = "ab\xe2\x82\xac";
120+
const size_t safe = stop_safe_prefix_len(text, {"XX"}, hit);
121+
EXPECT_FALSE(hit);
122+
EXPECT_EQ(safe, 2u); // only "ab"; the € is held whole
123+
}
124+
89125
} // namespace

extension/llm/runner/util.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <executorch/runtime/platform/compiler.h>
1414
#include <stdio.h>
1515
#include <time.h>
16+
#include <algorithm>
1617
#include <cctype>
1718
#include <string>
1819
#include <vector>
@@ -98,6 +99,55 @@ ET_EXPERIMENTAL size_t inline utf8_complete_prefix_len(const std::string& s) {
9899
return i;
99100
}
100101

102+
// How many leading bytes of `text` a streaming consumer may safely emit given a
103+
// set of `stops` strings, and whether a stop was hit (`stop_hit`).
104+
// * If any stop occurs, returns the byte offset of the EARLIEST occurrence and
105+
// sets stop_hit=true — text before it is safe; the stop and everything after
106+
// are dropped (the stop is excluded from output).
107+
// * Otherwise returns the length minus the longest possible partial-stop tail
108+
// (max(len(stop))-1 bytes), snapped DOWN to a UTF-8 boundary so a multi-byte
109+
// character is never split; stop_hit=false. Holding back that tail lets a
110+
// stop that straddles the next piece still be caught.
111+
// `text` is expected to be complete-UTF-8 (e.g. the assembled output of
112+
// utf8_complete_prefix_len). Empty `stops` => emit everything, no hold-back.
113+
ET_EXPERIMENTAL size_t inline stop_safe_prefix_len(
114+
const std::string& text,
115+
const std::vector<std::string>& stops,
116+
bool& stop_hit) {
117+
stop_hit = false;
118+
if (stops.empty()) {
119+
return text.size();
120+
}
121+
size_t earliest = std::string::npos;
122+
size_t max_len = 0;
123+
for (const auto& s : stops) {
124+
if (s.empty()) {
125+
continue;
126+
}
127+
max_len = std::max(max_len, s.size());
128+
const size_t p = text.find(s);
129+
if (p != std::string::npos &&
130+
(earliest == std::string::npos || p < earliest)) {
131+
earliest = p;
132+
}
133+
}
134+
if (earliest != std::string::npos) {
135+
stop_hit = true;
136+
return earliest;
137+
}
138+
const size_t hold = max_len > 0 ? max_len - 1 : 0;
139+
if (text.size() <= hold) {
140+
return 0;
141+
}
142+
size_t end = text.size() - hold;
143+
// Don't cut in the middle of a UTF-8 character: back up over continuation
144+
// bytes (10xxxxxx).
145+
while (end > 0 && (static_cast<unsigned char>(text[end]) & 0xC0) == 0x80) {
146+
--end;
147+
}
148+
return end;
149+
}
150+
101151
// ----------------------------------------------------------------------------
102152
// utilities: time
103153

0 commit comments

Comments
 (0)