Skip to content

Fix BFloat16 logits returned as garbage in Logits::Get() - #2203

Merged
kunal-vaishnavi merged 3 commits into
microsoft:mainfrom
justinchuby:justinchu/bf16-logits-cast
Jun 18, 2026
Merged

Fix BFloat16 logits returned as garbage in Logits::Get()#2203
kunal-vaishnavi merged 3 commits into
microsoft:mainfrom
justinchuby:justinchu/bf16-logits-cast

Conversation

@justinchuby

Copy link
Copy Markdown
Contributor

Summary

Logits::Get() only converted the model's raw logits to float32 when the output type was Float16. For BFloat16 the conversion was skipped, and the subsequent WrapTensor<float> reinterpreted the raw 2-byte bf16 values as 4-byte float32 — corrupting every logit (wrong argmax, incoherent generation). The identical model in Float16 worked correctly.

Fixes #2202.

Changes

  • src/models/logits.cpp: treat BFloat16 the same as Float16 in both the fp32 staging-buffer allocation and the Cast to float32.
  • src/cuda/model_kernels.cu + src/cuda/kernels.h: add LaunchBf16ToFp32 (on-device bf16→f32 conversion).
  • src/cuda/interface.cpp: route BFloat16 → float through the new kernel in the CUDA Cast. (The CPU Cast already supported bf16→f32, so the generic Cast helper otherwise falls back to a host round-trip.)

Verification

On a bf16 decoder (vocab 262144), the first-token argmax now matches the Float16 / HuggingFace reference, and greedy generation is byte-identical to the fp16 export. Before the fix, genai returned argmax 4539 (a value that did not correspond to any position in the model's actual output); after, it returns 496 (correct).

Logits::Get() only converted the model's raw logits to float32 when the
output type was Float16. For BFloat16 the conversion was skipped and the
subsequent WrapTensor<float> reinterpreted the raw 2-byte bf16 values as
4-byte float32, corrupting every logit (wrong argmax, incoherent
generation). The identical model in Float16 worked correctly.

Treat BFloat16 the same as Float16 in both the fp32 staging-buffer
allocation and the Cast to float32. Add an on-device CUDA bf16->f32 cast
(LaunchBf16ToFp32) so the conversion does not fall back to a host
round-trip; the CPU Cast path already supported bf16->f32.

Verified on a bf16 decoder (vocab 262144): first-token argmax now matches
the Float16 / HuggingFace reference and generation is identical to fp16.

Fixes microsoft#2202

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <11205048+justinchuby@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 9, 2026 17:06
@justinchuby
justinchuby requested a review from a team as a code owner June 9, 2026 17:06

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

Fixes incorrect logits returned by Logits::Get() when the model output tensor is BFloat16 by ensuring bf16 logits are cast to float32 (matching the existing fp16 behavior) and by adding a CUDA bf16→fp32 conversion path to keep the cast on-device.

Changes:

  • Treat Ort::BFloat16_t the same as Ort::Float16_t in Logits::Get() by allocating/using an fp32 staging tensor and invoking Cast() before wrapping logits as float.
  • Add a CUDA kernel entry point LaunchBf16ToFp32 for device-side bf16→fp32 conversion.
  • Route CUDA Cast() for BFloat16 → float through the new kernel.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/models/logits.cpp Enables bf16 logits to be converted to fp32 before exposing them via WrapTensor<float>().
src/cuda/model_kernels.cu Adds device kernel implementation for bf16→fp32 conversion.
src/cuda/kernels.h Declares the new CUDA bf16→fp32 launcher.
src/cuda/interface.cpp Adds CUDA Cast() dispatch for bf16→fp32 using the new kernel.

Comment thread src/models/logits.cpp
kunal-vaishnavi
kunal-vaishnavi previously approved these changes Jun 9, 2026

@kunal-vaishnavi kunal-vaishnavi 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.

Typically, we have upcasted the logits to float32 inside the model itself.

# Always use float32 logits to improve accuracy in the case of bf16 models.
if self.io_dtype == ir.DataType.BFLOAT16:
self.output_types["logits"] = ir.DataType.FLOAT

But I think it is still good to have the upcasting option inside ORT GenAI as a fallback.

Comment thread src/cuda/model_kernels.cu Outdated
Comment thread src/cuda/model_kernels.cu Outdated
Co-authored-by: Baiju Meswani <baijumeswani@gmail.com>
@justinchuby

Copy link
Copy Markdown
Contributor Author

@kunal-vaishnavi @baijumeswani could you approve? Thanks.

@justinchuby
justinchuby enabled auto-merge (squash) June 11, 2026 18:26
@baijumeswani

Copy link
Copy Markdown
Collaborator

@justinchuby can you merge main to trigger the integration test pipeline?

@justinchuby

Copy link
Copy Markdown
Contributor Author

/azp run Integration Tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines could not run because the pipeline triggers exclude this branch/path.

@kunal-vaishnavi

Copy link
Copy Markdown
Contributor

We need to sync with the main branch of ORT GenAI to get the changes needed to run the integration tests.

@justinchuby

Copy link
Copy Markdown
Contributor Author

Doesn't seem to be triggered 🤔

@microsoft microsoft deleted a comment from azure-pipelines Bot Jun 12, 2026
@kunal-vaishnavi

Copy link
Copy Markdown
Contributor

/azp run Integration Tests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@kunal-vaishnavi
kunal-vaishnavi disabled auto-merge June 18, 2026 21:12
@kunal-vaishnavi
kunal-vaishnavi merged commit 81b4926 into microsoft:main Jun 18, 2026
16 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BFloat16 logits returned as garbage — Logits::Get() only casts Float16 to float32

5 participants