Skip to content

[ML] Add byte_level_bpe ML tokenization with merges - #147217

Merged
edsavage merged 20 commits into
elastic:mainfrom
edsavage:wip/byte-level-bpe-tokenization
May 7, 2026
Merged

edsavage merged 20 commits into
elastic:mainfrom
edsavage:wip/byte-level-bpe-tokenization

Conversation

@edsavage

@edsavage edsavage commented Apr 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • New ByteLevelBpeTokenization / ByteLevelBpeTokenizationUpdate with XContent and named writeables
  • ByteLevelBpeTokenizer wired from NlpTokenizer.build using existing BPE byte-level + merge machinery
  • Registration in MlInferenceNamedXContentProvider and NlpConfigUpdate
  • Unit tests for config serialization, updates, and tokenizer parity with RoBERTa-style defaults on the shared test fixture

Before marking ready

  • Confirm CI / broader test matrix
  • Any follow-ups from maintainers (docs, REST examples, BWC notes, etc.)

Relates elastic/eland#818

Adds ByteLevelBpeTokenization, ByteLevelBpeTokenizer, XContent and
named-writeable registration, NlpTokenizer routing, and tests.
Special token strings are configurable with RoBERTa-style defaults.

Made-with: Cursor
NlpConfigUpdate.tokenizationFromMap lists known tokenizer names
including byte_level_bpe; update the expected exception substring.

Made-with: Cursor

Copilot AI 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

This PR introduces a new byte_level_bpe tokenization option for ML NLP inference in Elasticsearch Java, including merge-table support, serialization/parsing plumbing, and tokenizer wiring alongside existing BPE-based tokenizers (e.g., RoBERTa).

Changes:

  • Add ByteLevelBpeTokenization and ByteLevelBpeTokenizationUpdate with XContent + named writeable registration.
  • Implement ByteLevelBpeTokenizer and wire it into NlpTokenizer.build(...).
  • Add unit tests for tokenizer behavior and config/update serialization scaffolding.

Reviewed changes

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

Show a summary per file
File Description
x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizerTests.java Adds tokenizer parity tests vs RoBERTa-style defaults and multi-seq behavior.
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/NlpTokenizer.java Wires ByteLevelBpeTokenization to build a ByteLevelBpeTokenizer.
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizer.java Implements the byte-level BPE tokenizer using existing BPE analyzer machinery and configurable special tokens.
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfigTests.java Includes ByteLevelBpeTokenization in random test generation for embedding configs.
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/NlpConfigUpdateTests.java Updates expected “unknown tokenization type” message to include byte_level_bpe.
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/InferenceConfigTestScaffolding.java Extends test scaffolding to clone/mutate byte-level BPE tokenization + updates.
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationUpdateTests.java Adds apply + wire serialization test coverage for tokenization updates.
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationTests.java Adds XContent + wire serialization tests for the new tokenization config.
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/NlpConfigUpdate.java Registers byte_level_bpe tokenization update parsing in config updates.
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationUpdate.java Implements update application logic for ByteLevelBpeTokenization.
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java Adds the new tokenization config, parsing, serialization, and merges-required validation.
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/MlInferenceNamedXContentProvider.java Registers named XContent parsers and named writeables for the new tokenization + update.

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

Comment on lines +51 to +74
Tokenization.validateSpanAndTruncate(getTruncate(), getSpan());

if (getTruncate() != null && getTruncate().isInCompatibleWithSpan() == false) {
return new ByteLevelBpeTokenization(
byteLevelBpeTokenization.doLowerCase(),
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
getTruncate(),
null,
byteLevelBpeTokenization.isAddPrefixSpace(),
byteLevelBpeTokenization.getUnkToken(),
byteLevelBpeTokenization.getPadToken(),
byteLevelBpeTokenization.getBosToken(),
byteLevelBpeTokenization.getEosToken(),
byteLevelBpeTokenization.getMaskToken()
);
}

return new ByteLevelBpeTokenization(
byteLevelBpeTokenization.doLowerCase(),
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
Optional.ofNullable(this.getTruncate()).orElse(originalConfig.getTruncate()),
Optional.ofNullable(this.getSpan()).orElse(originalConfig.getSpan()),

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

The span-clearing logic here is inverted. Truncate.isInCompatibleWithSpan() returns true for FIRST/SECOND/BALANCED and false for NONE; the current == false branch triggers only for NONE and clears the span. This causes updates that set truncate to FIRST/SECOND/BALANCED to incorrectly keep an existing span (potentially throwing in the Tokenization constructor), and updates that set truncate to NONE to unnecessarily drop windowing. Flip the condition so span is cleared when the new truncate is incompatible with span, and ensure the returned config is always valid.

Suggested change
Tokenization.validateSpanAndTruncate(getTruncate(), getSpan());
if (getTruncate() != null && getTruncate().isInCompatibleWithSpan() == false) {
return new ByteLevelBpeTokenization(
byteLevelBpeTokenization.doLowerCase(),
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
getTruncate(),
null,
byteLevelBpeTokenization.isAddPrefixSpace(),
byteLevelBpeTokenization.getUnkToken(),
byteLevelBpeTokenization.getPadToken(),
byteLevelBpeTokenization.getBosToken(),
byteLevelBpeTokenization.getEosToken(),
byteLevelBpeTokenization.getMaskToken()
);
}
return new ByteLevelBpeTokenization(
byteLevelBpeTokenization.doLowerCase(),
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
Optional.ofNullable(this.getTruncate()).orElse(originalConfig.getTruncate()),
Optional.ofNullable(this.getSpan()).orElse(originalConfig.getSpan()),
Tokenization.Truncate truncate = Optional.ofNullable(this.getTruncate()).orElse(originalConfig.getTruncate());
Integer span = Optional.ofNullable(this.getSpan()).orElse(originalConfig.getSpan());
if (getTruncate() != null && getTruncate().isInCompatibleWithSpan()) {
span = null;
}
Tokenization.validateSpanAndTruncate(truncate, span);
return new ByteLevelBpeTokenization(
byteLevelBpeTokenization.doLowerCase(),
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
truncate,
span,

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm deliberately leaving the logic as-is for parity with existing tokenizers and the tests (RobertaTokenizationUpdateTests / ByteLevelBpeTokenizationUpdateTests).

ByteLevelBpeTokenizationUpdate#apply intentionally matches RobertaTokenizationUpdate#apply (and the other *TokenizationUpdate types): validate the update’s own truncate/span, then take the special path when getTruncate() != null && getTruncate().isInCompatibleWithSpan() == false - i.e. when the update explicitly sets Truncate.NONE, which is the only enum whose isInCompatibleWithSpan() is false. That branch forces span to unset so a partial update that sets truncate: none doesn’t inherit a previous sliding-window span. Flipping the predicate would drop that NONE behaviour and instead clear span only for first/second/balanced, which would diverge from Roberta unless we changed all tokenizers together - and it would still be a behaviour change (e.g. updates that currently throw when merged truncate/span is invalid would start succeeding with span coerced away).

The Roberta comment above that if is misleading (it says “incompatible” but the condition is the NONE / span-compatible case); we can fix that separately rather than changing the predicate only on this class.

Comment on lines +42 to +55
var updatedTruncate = new ByteLevelBpeTokenizationUpdate(Tokenization.Truncate.FIRST, null).apply(
new ByteLevelBpeTokenization(true, true, 512, Tokenization.Truncate.SECOND, null, true, null, null, null, null, null)
);
assertEquals(
new ByteLevelBpeTokenization(true, true, 512, Tokenization.Truncate.FIRST, null, true, null, null, null, null, null),
updatedTruncate
);

var updatedNone = new ByteLevelBpeTokenizationUpdate(Tokenization.Truncate.NONE, null).apply(
new ByteLevelBpeTokenization(true, true, 512, Tokenization.Truncate.SECOND, null, false, null, null, null, null, null)
);
assertEquals(
new ByteLevelBpeTokenization(true, true, 512, Tokenization.Truncate.NONE, null, false, null, null, null, null, null),
updatedNone

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

testApply doesn’t currently cover the important transition from a windowing config (truncate=none + span set) to an incompatible truncate (FIRST/SECOND/BALANCED), where apply() should clear the span rather than throwing. Adding an assertion for that case would have caught the span-handling bug in ByteLevelBpeTokenizationUpdate.apply() and helps prevent regressions.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Today, with the code as it stands, apply() doesn’t strip the old span when you go from none + sliding window to first/second/balanced and leave span out of the update - it merges in the previous span and Tokenization’s ctor blows up with IllegalArgumentException. That’s eactly the same story as Roberta (and friends), not something ByteLevel BPE is doing differently.

So a test that expects span to be auto-cleared is really arguing for a new behaviour we’d want everywhere, not just BPE.

I've added tests for what we have today. An expectThrows for that transition has been added for all TokenizationUpdateTests.

Split the expected error substring so test sources stay within the 140-column limit.

Made-with: Cursor
Guard with truncate != null so null truncate does not clear span.
That restores span-only random instances for BWC serialization tests
across all tokenizer update test classes.

Made-with: Cursor
@edsavage

Copy link
Copy Markdown
Contributor Author

There is noticeable duplication across the various *TokenizationUpdate classes (and their tests): shared fields and parsing live in AbstractTokenizationUpdate, but each tokenizer still repeats the same parser scaffold and the same merge shape in apply().

Consolidating that (for example a shared helper or a small per-tokenizer strategy for the new …Tokenization(...) constructor tails) would be a worthwhile cleanup, but it is out of scope for this PR. This change set is focused on adding byte-level BPE support and the related tests and docs, without refactoring the whole tokenizer-update family.

Add expectThrows for NONE+sliding span merged with FIRST-only updates
across all TokenizationUpdateTests, matching Tokenization validation.

Made-with: Cursor
@edsavage edsavage changed the title WIP: Add byte_level_bpe ML tokenization with merges Add byte_level_bpe ML tokenization with merges Apr 30, 2026
@edsavage
edsavage marked this pull request as ready for review April 30, 2026 01:58
@edsavage
edsavage requested a review from Copilot April 30, 2026 01:58
@elasticsearchmachine elasticsearchmachine added the needs:triage Requires assignment of a team area label label Apr 30, 2026

Copilot AI 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 17 out of 17 changed files in this pull request and generated 2 comments.


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

Comment on lines +92 to +112
public ByteLevelBpeTokenization(
@Nullable Boolean doLowerCase,
@Nullable Boolean withSpecialTokens,
@Nullable Integer maxSequenceLength,
@Nullable Truncate truncate,
@Nullable Integer span,
@Nullable Boolean addPrefixSpace,
@Nullable String unkToken,
@Nullable String padToken,
@Nullable String bosToken,
@Nullable String eosToken,
@Nullable String maskToken
) {
super(doLowerCase, withSpecialTokens, maxSequenceLength, truncate, span);
this.addPrefixSpace = Optional.ofNullable(addPrefixSpace).orElse(DEFAULT_ADD_PREFIX_SPACE);
this.unkToken = resolveSpecialToken(unkToken, DEFAULT_UNK_TOKEN);
this.padToken = resolveSpecialToken(padToken, DEFAULT_PAD_TOKEN);
this.bosToken = resolveSpecialToken(bosToken, DEFAULT_BOS_TOKEN);
this.eosToken = resolveSpecialToken(eosToken, DEFAULT_EOS_TOKEN);
this.maskToken = resolveSpecialToken(maskToken, DEFAULT_MASK_TOKEN);
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

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

do_lower_case is accepted here and passed to super(...), but the runtime tokenizer (ByteLevelBpeTokenizer/BpeAnalyzer) does not apply lowercasing (unlike WordPiece tokenizers). This makes do_lower_case=true a silent no-op that can change expected token IDs and is inconsistent with RobertaTokenization/XLMRobertaTokenization, which explicitly reject do_lower_case=true. Consider either (a) rejecting do_lower_case=true with an IllegalArgumentException and forcing super(false, ...), or (b) actually applying lowercasing in the analyzer/tokenizer pipeline so the setting has a real effect.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +83
if (withSpecialTokens) {
Set<String> missingSpecialTokens = Sets.difference(Set.of(eosToken, bosToken), vocab.keySet());
if (missingSpecialTokens.isEmpty() == false) {
throw ExceptionsHelper.conflictStatusException("stored vocabulary is missing required {} token(s)", missingSpecialTokens);
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

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

Set.of(eosToken, bosToken) will throw an IllegalArgumentException if a user configures bos_token and eos_token to the same string (duplicate element). Since these token strings are configurable, it’s better to either validate they’re distinct with a clear error message, or build the required-token set in a way that tolerates duplicates (e.g., via a mutable set) before calling Sets.difference(...).

Copilot uses AI. Check for mistakes.
@edsavage edsavage added >enhancement :ml Machine learning labels Apr 30, 2026
@elasticsearchmachine elasticsearchmachine added Team:ML Meta label for the ML team and removed needs:triage Requires assignment of a team area label labels Apr 30, 2026
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Hi @edsavage, I've created a changelog YAML for you.

@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Pinging @elastic/ml-core (Team:ML)

@github-actions

github-actions Bot commented Apr 30, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Preview links for changed docs

⏳ Building and deploying preview... View progress

This comment will be updated with preview links when the build is complete.

@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Important: Docs version tagging

👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version.

We use applies_to tags to mark version-specific features and changes.

Expand for a quick overview

When to use applies_to tags:

✅ At the page level to indicate which products/deployments the content applies to (mandatory)
✅ When features change state (e.g. preview, ga) in a specific version
✅ When availability differs across deployments and environments

What NOT to do:

❌ Don't remove or replace information that applies to an older version
❌ Don't add new information that applies to a specific version without an applies_to tag
❌ Don't forget that applies_to tags can be used at the page, section, and inline level

🤔 Need help?

Match Roberta by rejecting do_lower_case=true via delegating ctor and
wire checks. Use HashSet for required BOS/EOS when validating vocab so
identical configured strings do not trip Set.of. Add tests; fix BPE
fixtures that passed true in the do_lower_case slot.

Made-with: Cursor
Document RobertaTokenizationResult reuse and mask vocab behavior; pass false for do_lower_case in tokenizer builder; align NlpConfigUpdate map key with ByteLevelBpeTokenizationUpdate.NAME; add NlpConfigUpdateTests and ml yamlRestTest for byte_level_bpe text_embedding config.

Made-with: Cursor
Replace x-pack plugin YAML suite coverage with ByteLevelBpeInferenceConfigRestIT under ml single-node-tests so classpath suite resolution is unchanged.

Made-with: Cursor

Copilot AI 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 19 out of 19 changed files in this pull request and generated 2 comments.


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

Comment on lines +821 to +823
namedWriteables.add(
new NamedWriteableRegistry.Entry(Tokenization.class, ByteLevelBpeTokenization.NAME, ByteLevelBpeTokenization::new)
);
Comment on lines +848 to 854
ByteLevelBpeTokenizationUpdate.NAME.getPreferredName(),
ByteLevelBpeTokenizationUpdate::new
)
);
namedWriteables.add(
new NamedWriteableRegistry.Entry(
TokenizationUpdate.class,
Add ml_byte_level_bpe_tokenization transport version, reject serialization to older nodes from ByteLevelBpeTokenization and ByteLevelBpeTokenizationUpdate, and raise TextEmbedding config minimal supported version when byte-level BPE is used. Update BWC tests and TextEmbeddingConfigTests bwcVersions filter.
@valeriy42
valeriy42 self-requested a review May 5, 2026 18:48
@valeriy42

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds comprehensive support for Byte-Level BPE tokenization to Elasticsearch's ML inference system, including new tokenization configuration classes, a tokenizer implementation, registry integration, and extensive test coverage.

Changes

Byte-Level BPE Tokenization Support

Layer / File(s) Summary
Data Shape
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java
New ByteLevelBpeTokenization class defines tokenization config for byte-level BPE models with fields for add_prefix_space and special tokens (unk_token, pad_token, bos_token, eos_token, mask_token), validation preventing do_lower_case=true, transport-version gating, and full serialization/deserialization support.
Update Data Shape
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationUpdate.java
New ByteLevelBpeTokenizationUpdate class enables updates to ByteLevelBpeTokenization instances with truncate and span parameters, including validation logic and conditional construction.
Core Tokenizer
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizer.java
Implements ByteLevelBpeTokenizer backed by BpeTokenizer/BpeAnalyzer with RoBERTa-style tokenization, special token handling, token ID resolution, and a Builder for configuration.
Registry Integration
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/MlInferenceNamedXContentProvider.java
Registers ByteLevelBpeTokenization and ByteLevelBpeTokenizationUpdate in named XContent and writeable registries for serialization/parsing.
Config Integration
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfig.java, TextEmbeddingConfigUpdate.java, NlpConfigUpdate.java, NlpTokenizer.java
Adds ByteLevelBpeTokenization support to text embedding configs, update mapping, tokenizer factory, and transport version gating.
Core Tests
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationTests.java, ByteLevelBpeTokenizationUpdateTests.java
Comprehensive backward-compatibility, serialization, parsing, and validation tests for both tokenization classes; includes span variants and negative cases.
Tokenizer Tests
x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizerTests.java
Tests validate RoBERTa-style parity, multi-sequence tokenization, and special-token behavior.
Test Infrastructure Updates
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/InferenceConfigTestScaffolding.java, TextEmbeddingConfigTests.java, NlpConfigUpdateTests.java
Scaffolding and test utilities extended to handle ByteLevelBpeTokenization in version mutation, cloning, and update creation; test data generation includes ByteLevelBpeTokenization paths.
Tokenization Update Tests (Fixes)
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/BertTokenizationUpdateTests.java, BertJapaneseTokenizationUpdateTests.java, RobertaTokenizationUpdateTests.java, MPNetTokenizationUpdateTests.java, XLMRobertaTokenizationUpdateTests.java
Null-safety improvements to randomInstance() methods (guard span nullification with truncate non-null check); new tests for incompatible truncate-with-inherited-span scenarios across all tokenization update classes.
Integration Tests
x-pack/plugin/ml/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/ByteLevelBpeInferenceConfigRestIT.java
REST integration test validates round-tripping of a trained model with text_embedding using byte_level_bpe tokenization, including config retrieval and field validation.
Documentation
docs/changelog/147217.yaml
Changelog entry documents the enhancement of byte-level BPE ML tokenization addition.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A byte-level hopper bounds with glee,
Through BPE tokens wild and free,
New registries hum with special care,
RoBERTa's style floats through the air,
Sphinx-like tests ensure all's right—
Tokenization shines so bright! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is well-organized and directly related to the changeset, covering the new tokenization types, implementation details, registration, tests, and next steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding byte_level_bpe ML tokenization with merges support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java (2)

104-132: 💤 Low value

Validate doLowerCase before delegating, to surface the right error first.

The public constructor delegates to the private ctor (which calls super(false, ...) and runs validateSpanAndTruncate) before checking Boolean.TRUE.equals(doLowerCase). If a caller passes both do_lower_case=true and an invalid span/truncate combination, the user gets a span/truncate error rather than the more diagnostic "do_lower_case=true not allowed" error. Consider extracting the validation into a static helper invoked as the first argument expression so it runs prior to delegation.

♻️ Suggested refactor
     public ByteLevelBpeTokenization(
         `@Nullable` Boolean doLowerCase,
         `@Nullable` Boolean withSpecialTokens,
         ...
         `@Nullable` String maskToken
     ) {
         this(
-            withSpecialTokens,
+            validateDoLowerCase(doLowerCase) ? withSpecialTokens : withSpecialTokens,
             maxSequenceLength,
             ...
         );
-        if (Boolean.TRUE.equals(doLowerCase)) {
-            throw new IllegalArgumentException("unable to set [do_lower_case] to [true] for byte_level_bpe tokenizer");
-        }
+    }
+
+    private static boolean validateDoLowerCase(`@Nullable` Boolean doLowerCase) {
+        if (Boolean.TRUE.equals(doLowerCase)) {
+            throw new IllegalArgumentException("unable to set [do_lower_case] to [true] for byte_level_bpe tokenizer");
+        }
+        return true;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java`
around lines 104 - 132, Move the doLowerCase check so it runs before the
constructor delegation: add a small static helper method (e.g.,
validateDoLowerCaseOrThrow(Boolean doLowerCase)) that throws the
IllegalArgumentException when Boolean.TRUE.equals(doLowerCase), and call that
helper as the first argument expression in the public
ByteLevelBpeTokenization(...) constructor invocation so the validation executes
before delegating to the private constructor (the private ctor and
validateSpanAndTruncate remain unchanged).

155-166: 💤 Low value

Defensive check against stream corruption or version mismatch is appropriate, but error message should reflect this.

The check in the StreamInput constructor (lines 157-159) is unreachable under normal operation: writeTo enforces ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED version compatibility, the public constructor rejects doLowerCase=true, and all internal construction paths explicitly pass false. However, the check correctly handles corrupted or mismatched stream data. Consider clarifying the error message to indicate this is a version mismatch or data corruption scenario rather than a normal validation error—something like: "Unexpected [do_lower_case=true] in stream; indicates version mismatch or data corruption".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java`
around lines 155 - 166, The StreamInput constructor
ByteLevelBpeTokenization(StreamInput in) currently throws an
IllegalArgumentException with a generic message when doLowerCase is true; update
that exception message to clearly indicate this is unexpected in a stream and
likely due to a version mismatch or stream corruption (e.g., "Unexpected
[do_lower_case=true] in stream; indicates version mismatch or data corruption")
so callers can distinguish stream corruption/version issues from normal
validation; locate the check in the ByteLevelBpeTokenization(StreamInput in)
constructor and replace the existing message accordingly.
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizer.java (1)

67-99: ⚡ Quick win

Mask token is never null; reorder validations before BpeAnalyzer allocation to improve resource handling.

The null mask-token concern is unfounded. ByteLevelBpeTokenization always applies default values via resolveSpecialToken in its constructor, guaranteeing all token strings (including maskToken) are non-null when passed to this constructor. Therefore, Set.of(maskToken) on line 70 and subsequent uses cannot throw NullPointerException.

The resource ordering concern is valid but minor. BpeAnalyzer is allocated at line 67 before the unk/pad/bos/eos validations at lines 77–98 run. If any validation throws, the analyzer instance is created but never assigned to the field, and close() is never called. While BpeAnalyzer does not hold critical resources (no file handles or network connections), moving the validations before instantiation would be cleaner:

// Validate tokens against vocab before allocating BpeAnalyzer
if (vocab.containsKey(unkToken) == false) {
    throw ExceptionsHelper.conflictStatusException("stored vocabulary is missing required [{}] token", unkToken);
}
// ... other validations ...

// Then allocate analyzer
this.bpeAnalyzer = new BpeAnalyzer(...);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizer.java`
around lines 67 - 99, The constructor currently instantiates BpeAnalyzer
(this.bpeAnalyzer) before validating required tokens (unkToken, padToken,
bosToken, eosToken), which can leave an owned analyzer created when a validation
later throws; move all vocabulary/token existence checks
(vocab.containsKey(unkToken), vocab.containsKey(padToken), and when
withSpecialTokens check for eosToken/bosToken) to run before allocating new
BpeAnalyzer so the analyzer is only created after validations pass; refer to the
ByteLevelBpeTokenizer constructor, maskToken usage in the Set.of(...) call, the
bpeAnalyzer field, and the token id fields (padTokenId, sepTokenId, clsTokenId)
when making this change and ensure analyzer allocation and assignment happen
after those checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java`:
- Around line 26-37: The Javadoc incorrectly implies BOS/EOS vocabulary presence
is validated in ByteLevelBpeTokenization.validateVocabulary(); update the
docstring to state that the requirement is enforced when building/instantiating
the tokenizer (ByteLevelBpeTokenizer constructor) to match actual behavior (and
be consistent with RobertaTokenization and XLMRobertaTokenization), or
alternatively move the HashSet-based presence check into
ByteLevelBpeTokenization.validateVocabulary() if you want vocabulary-upload-time
validation; reference ByteLevelBpeTokenization.validateVocabulary() and
ByteLevelBpeTokenizer (the constructor where the current conflict exception is
thrown) when making the change.

In
`@x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/InferenceConfigTestScaffolding.java`:
- Around line 47-60: The clone of ByteLevelBpeTokenization incorrectly hardcodes
the first constructor argument to false, losing the original doLowerCase()
value; update the cloning block in InferenceConfigTestScaffolding so that the
first argument uses byteLevelBpeTokenization.doLowerCase() (preserving
null/true/false) instead of false while keeping all other fields the same
(withSpecialTokens(), maxSequenceLength(), truncate, getSpan(),
isAddPrefixSpace(), getUnkToken(), getPadToken(), getBosToken(), getEosToken(),
getMaskToken()) so only truncation is changed.

In
`@x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfigTests.java`:
- Around line 25-35: The current override of bwcVersions() filters out
pre-ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED versions which removes full BWC
coverage for non-byte-level-BPE TextEmbeddingConfig variants; revert
bwcVersions() to return getAllBWCVersions() (i.e., remove the .filter(...) call)
so general TextEmbeddingConfigTests keep full serialization matrix, and instead
add a dedicated test or branch that exercises createRandom() instances
containing ByteLevelBpeTokenization guarded by checking
ByteLevelBpeTokenization.ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED (e.g., a new test
method that filters versions and explicitly serializes/deserializes instances
produced by createRandom() that include ByteLevelBpeTokenization) so only that
case is version-gated while others remain unfiltered.

---

Nitpick comments:
In
`@x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java`:
- Around line 104-132: Move the doLowerCase check so it runs before the
constructor delegation: add a small static helper method (e.g.,
validateDoLowerCaseOrThrow(Boolean doLowerCase)) that throws the
IllegalArgumentException when Boolean.TRUE.equals(doLowerCase), and call that
helper as the first argument expression in the public
ByteLevelBpeTokenization(...) constructor invocation so the validation executes
before delegating to the private constructor (the private ctor and
validateSpanAndTruncate remain unchanged).
- Around line 155-166: The StreamInput constructor
ByteLevelBpeTokenization(StreamInput in) currently throws an
IllegalArgumentException with a generic message when doLowerCase is true; update
that exception message to clearly indicate this is unexpected in a stream and
likely due to a version mismatch or stream corruption (e.g., "Unexpected
[do_lower_case=true] in stream; indicates version mismatch or data corruption")
so callers can distinguish stream corruption/version issues from normal
validation; locate the check in the ByteLevelBpeTokenization(StreamInput in)
constructor and replace the existing message accordingly.

In
`@x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizer.java`:
- Around line 67-99: The constructor currently instantiates BpeAnalyzer
(this.bpeAnalyzer) before validating required tokens (unkToken, padToken,
bosToken, eosToken), which can leave an owned analyzer created when a validation
later throws; move all vocabulary/token existence checks
(vocab.containsKey(unkToken), vocab.containsKey(padToken), and when
withSpecialTokens check for eosToken/bosToken) to run before allocating new
BpeAnalyzer so the analyzer is only created after validations pass; refer to the
ByteLevelBpeTokenizer constructor, maskToken usage in the Set.of(...) call, the
bpeAnalyzer field, and the token id fields (padTokenId, sepTokenId, clsTokenId)
when making this change and ensure analyzer allocation and assignment happen
after those checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Enterprise

Run ID: 1c9c1b2b-d65e-465e-a887-d4eb028ac17a

📥 Commits

Reviewing files that changed from the base of the PR and between 4a49d2b and 4b83ce6.

⛔ Files ignored due to path filters (2)
  • server/src/main/resources/transport/definitions/referable/ml_byte_level_bpe_tokenization.csv is excluded by !**/*.csv
  • server/src/main/resources/transport/upper_bounds/9.5.csv is excluded by !**/*.csv
📒 Files selected for processing (21)
  • docs/changelog/147217.yaml
  • x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/MlInferenceNamedXContentProvider.java
  • x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java
  • x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationUpdate.java
  • x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/NlpConfigUpdate.java
  • x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfig.java
  • x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfigUpdate.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/BertJapaneseTokenizationUpdateTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/BertTokenizationUpdateTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenizationUpdateTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/InferenceConfigTestScaffolding.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/MPNetTokenizationUpdateTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/NlpConfigUpdateTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/RobertaTokenizationUpdateTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfigTests.java
  • x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/XLMRobertaTokenizationUpdateTests.java
  • x-pack/plugin/ml/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/ByteLevelBpeInferenceConfigRestIT.java
  • x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizer.java
  • x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/NlpTokenizer.java
  • x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/inference/nlp/tokenizers/ByteLevelBpeTokenizerTests.java

Comment on lines +26 to +37
/**
* Tokenization settings for byte-level BPE models (for example GPT-2 style vocabularies) that ship
* a merge table alongside the vocabulary. The in-process tokenizer applies UTF-8 byte mapping and
* BPE merges using the same engine as the RoBERTa tokenizer, but special token strings are
* configurable so deployments can align with Hugging Face tokenizer metadata.
* {@code do_lower_case} is not applied by the byte-level BPE analyzer and must not be set to {@code true};
* requests that set it to {@code true} are rejected like {@link RobertaTokenization}.
* When {@code with_special_tokens} is {@code true}, the configured BOS and EOS strings must be present
* in the vocabulary; the configured mask string is optional for encoding-only inference, and the mask
* token id may be unset if that token is missing from the vocabulary (masking-oriented tasks should
* ensure it exists).
*/

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where BOS/EOS validation is performed for ByteLevelBpe / Roberta-style tokenization.
fd -e java ByteLevelBpeTokenizer
rg -nP --type=java -C5 '\b(bosToken|eosToken|getBosToken|getEosToken)\b' -g '!**/build/**'
rg -nP --type=java -C3 'HashSet' -g '*ByteLevelBpe*'
rg -nP --type=java -C5 'validateVocabulary' -g '*Tokenization*.java'

Repository: elastic/elasticsearch

Length of output: 44862


BOS/EOS-in-vocabulary validation is present; validation timing differs from documented location.

The class Javadoc documents that "When with_special_tokens is true, the configured BOS and EOS strings must be present in the vocabulary." This constraint is enforced, but in ByteLevelBpeTokenizer (lines 84–92 in x-pack/plugin/ml/src/main/java/...), not in ByteLevelBpeTokenization.validateVocabulary(). The validation uses a HashSet to check that configured BOS and EOS tokens exist in the vocabulary, throwing a conflict exception if missing. This validation occurs at tokenizer instantiation time (when the model is built/loaded), not during vocabulary upload validation. The pattern matches other tokenizations (RobertaTokenization, XLMRobertaTokenization), where validateVocabulary() checks only format-specific requirements (non-empty merges/scores) and semantic constraints are enforced in the tokenizer constructor. Consider clarifying the Javadoc to indicate that the constraint is enforced at tokenizer build time rather than at vocabulary upload, or add an explicit check in validateVocabulary() for earlier failure detection if desired.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/ByteLevelBpeTokenization.java`
around lines 26 - 37, The Javadoc incorrectly implies BOS/EOS vocabulary
presence is validated in ByteLevelBpeTokenization.validateVocabulary(); update
the docstring to state that the requirement is enforced when
building/instantiating the tokenizer (ByteLevelBpeTokenizer constructor) to
match actual behavior (and be consistent with RobertaTokenization and
XLMRobertaTokenization), or alternatively move the HashSet-based presence check
into ByteLevelBpeTokenization.validateVocabulary() if you want
vocabulary-upload-time validation; reference
ByteLevelBpeTokenization.validateVocabulary() and ByteLevelBpeTokenizer (the
constructor where the current conflict exception is thrown) when making the
change.

Comment on lines +47 to +60
} else if (tokenization instanceof ByteLevelBpeTokenization byteLevelBpeTokenization) {
return new ByteLevelBpeTokenization(
false,
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
truncate,
byteLevelBpeTokenization.getSpan(),
byteLevelBpeTokenization.isAddPrefixSpace(),
byteLevelBpeTokenization.getUnkToken(),
byteLevelBpeTokenization.getPadToken(),
byteLevelBpeTokenization.getBosToken(),
byteLevelBpeTokenization.getEosToken(),
byteLevelBpeTokenization.getMaskToken()
);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve doLowerCase() when cloning byte-level BPE tokenizations.

This helper is supposed to change truncation only, but it hard-codes the first constructor arg to false. ByteLevelBpeTokenization still allows do_lower_case to be unset, so this can produce the wrong expected config in update/version tests whenever the original instance had null there.

♻️ Suggested fix
         } else if (tokenization instanceof ByteLevelBpeTokenization byteLevelBpeTokenization) {
             return new ByteLevelBpeTokenization(
-                false,
+                byteLevelBpeTokenization.doLowerCase(),
                 byteLevelBpeTokenization.withSpecialTokens(),
                 byteLevelBpeTokenization.maxSequenceLength(),
                 truncate,
                 byteLevelBpeTokenization.getSpan(),
                 byteLevelBpeTokenization.isAddPrefixSpace(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (tokenization instanceof ByteLevelBpeTokenization byteLevelBpeTokenization) {
return new ByteLevelBpeTokenization(
false,
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
truncate,
byteLevelBpeTokenization.getSpan(),
byteLevelBpeTokenization.isAddPrefixSpace(),
byteLevelBpeTokenization.getUnkToken(),
byteLevelBpeTokenization.getPadToken(),
byteLevelBpeTokenization.getBosToken(),
byteLevelBpeTokenization.getEosToken(),
byteLevelBpeTokenization.getMaskToken()
);
} else if (tokenization instanceof ByteLevelBpeTokenization byteLevelBpeTokenization) {
return new ByteLevelBpeTokenization(
byteLevelBpeTokenization.doLowerCase(),
byteLevelBpeTokenization.withSpecialTokens(),
byteLevelBpeTokenization.maxSequenceLength(),
truncate,
byteLevelBpeTokenization.getSpan(),
byteLevelBpeTokenization.isAddPrefixSpace(),
byteLevelBpeTokenization.getUnkToken(),
byteLevelBpeTokenization.getPadToken(),
byteLevelBpeTokenization.getBosToken(),
byteLevelBpeTokenization.getEosToken(),
byteLevelBpeTokenization.getMaskToken()
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/InferenceConfigTestScaffolding.java`
around lines 47 - 60, The clone of ByteLevelBpeTokenization incorrectly
hardcodes the first constructor argument to false, losing the original
doLowerCase() value; update the cloning block in InferenceConfigTestScaffolding
so that the first argument uses byteLevelBpeTokenization.doLowerCase()
(preserving null/true/false) instead of false while keeping all other fields the
same (withSpecialTokens(), maxSequenceLength(), truncate, getSpan(),
isAddPrefixSpace(), getUnkToken(), getPadToken(), getBosToken(), getEosToken(),
getMaskToken()) so only truncation is changed.

Comment on lines +25 to +35
/**
* {@link #createRandom()} may use {@link ByteLevelBpeTokenization}, which requires
* {@link ByteLevelBpeTokenization#ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED}; the base class filters BWC versions using only one
* {@link #createTestInstance()} sample, so we must assume byte-level BPE may appear in every serialization run.
*/
@Override
protected List<TransportVersion> bwcVersions() {
return getAllBWCVersions().stream()
.filter(v -> v.supports(ByteLevelBpeTokenization.ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED))
.collect(Collectors.toList());
}

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep full BWC coverage for non-byte-level-BPE configs.

This override drops every pre-ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED version for the entire TextEmbeddingConfigTests class. Once createRandom() started emitting ByteLevelBpeTokenization, the older serialization matrix for existing tokenizations stopped being exercised here too. I’d keep the full BWC version list for the general TextEmbeddingConfig path and isolate byte-level BPE into a dedicated version-gated case instead of narrowing coverage for all variants.

Also applies to: 93-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/TextEmbeddingConfigTests.java`
around lines 25 - 35, The current override of bwcVersions() filters out
pre-ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED versions which removes full BWC
coverage for non-byte-level-BPE TextEmbeddingConfig variants; revert
bwcVersions() to return getAllBWCVersions() (i.e., remove the .filter(...) call)
so general TextEmbeddingConfigTests keep full serialization matrix, and instead
add a dedicated test or branch that exercises createRandom() instances
containing ByteLevelBpeTokenization guarded by checking
ByteLevelBpeTokenization.ML_BYTE_LEVEL_BPE_TOKENIZATION_ADDED (e.g., a new test
method that filters versions and explicitly serializes/deserializes instances
produced by createRandom() that include ByteLevelBpeTokenization) so only that
case is version-gated while others remain unfiltered.

@valeriy42 valeriy42 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.

LGTM once you resolve the BWC issues flagged by the code rabbit.

edsavage added 4 commits May 7, 2026 13:16
Clarify Javadoc for BOS/EOS validation timing; preserve doLowerCase in InferenceConfigTestScaffolding clone; restore full TextEmbeddingConfig BWC matrix by moving byte-level BPE serialization to TextEmbeddingConfigByteLevelBpeBwcTests.
Regenerate ml_byte_level_bpe_tokenization after merge with main so it no longer shares id 9373000 with voyage_ai_embedding_type_non_nullable (IllegalStateException in TransportVersion holder).
@edsavage edsavage changed the title Add byte_level_bpe ML tokenization with merges [ML] Add byte_level_bpe ML tokenization with merges May 7, 2026
@edsavage
edsavage merged commit 9ccaf75 into elastic:main May 7, 2026
38 checks passed
ncordon pushed a commit to ncordon/elasticsearch that referenced this pull request May 8, 2026
Adds ByteLevelBpeTokenization, ByteLevelBpeTokenizer, XContent and
named-writeable registration, NlpTokenizer routing, and tests.
Special token strings are configurable with RoBERTa-style defaults.
alighahramani-alig pushed a commit to alighahramani-alig/elasticsearch that referenced this pull request May 8, 2026
Adds ByteLevelBpeTokenization, ByteLevelBpeTokenizer, XContent and
named-writeable registration, NlpTokenizer routing, and tests.
Special token strings are configurable with RoBERTa-style defaults.
henningandersen pushed a commit to henningandersen/elasticsearch that referenced this pull request May 11, 2026
Adds ByteLevelBpeTokenization, ByteLevelBpeTokenizer, XContent and
named-writeable registration, NlpTokenizer routing, and tests.
Special token strings are configurable with RoBERTa-style defaults.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

>enhancement :ml Machine learning Team:ML Meta label for the ML team v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants