Skip to content

Commit dbf73b1

Browse files
authored
Merge branch 'main' into sukru/fix-offline-tests
2 parents 7d21aab + 49becc6 commit dbf73b1

6 files changed

Lines changed: 205 additions & 3 deletions

File tree

python/src/coreai_models/diffusion/pipeline.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,6 @@ def _load_hf_pipeline(model_id: str, pipeline_type: str, model_dtype: torch.dtyp
162162
from diffusers import Flux2KleinPipeline
163163

164164
hf_pipe = Flux2KleinPipeline.from_pretrained(model_id, torch_dtype=model_dtype)
165-
# Text encoder needs float32 for token embedding precision
166-
hf_pipe.text_encoder = hf_pipe.text_encoder.float()
167165
return hf_pipe
168166

169167
if pipeline_type == "sd3":

swift/Sources/CoreAILanguageModels/GuidedGeneration/ConstrainedGenerationSession.swift

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import Tokenizers
1717
/// Each session is tied to a specific JSON schema and vocabulary. It tracks
1818
/// the generation state and produces token masks that enforce schema compliance.
1919
public struct ConstrainedGenerationSession: ~Copyable {
20+
static let maxRollbackTokens = 64
21+
2022
private let tokenizerInfo: TokenizerInfo
2123
private let compiler: GrammarCompiler
2224
private let compiledGrammar: CompiledGrammar
@@ -93,7 +95,7 @@ public struct ConstrainedGenerationSession: ~Copyable {
9395
self.tokenizerInfo = tokenizerInfo
9496
self.compiler = GrammarCompiler(tokenizerInfo: tokenizerInfo)
9597
self.compiledGrammar = try compiler.compileJSONSchema(jsonSchema)
96-
self.matcher = GrammarMatcher(compiledGrammar: compiledGrammar)
98+
self.matcher = GrammarMatcher(compiledGrammar: compiledGrammar, maxRollbackTokens: Self.maxRollbackTokens)
9799
self.vocabularySize = tokenizerInfo.vocabularySize
98100
self.bitmaskSize = (vocabularySize + 31) / 32
99101
self.bitmaskBuffer = Array(repeating: 0, count: bitmaskSize)
@@ -193,6 +195,50 @@ public struct ConstrainedGenerationSession: ~Copyable {
193195
matcher.reset()
194196
allTokensBlocked = false
195197
}
198+
199+
/// Rollback the grammar state by N tokens. Returns false if rollback failed
200+
/// (e.g., exceeds maxRollbackTokens budget).
201+
@discardableResult
202+
public mutating func rollback(_ numTokens: Int = 1) -> Bool {
203+
guard numTokens >= 0 else { return false }
204+
return matcher.rollback(numTokens)
205+
}
206+
207+
/// Find the longest deterministic string from the current grammar state.
208+
/// Does not change the matcher state. Returns nil if no jump-forward is possible.
209+
public func findJumpForwardString() -> String? {
210+
matcher.findJumpForwardString()
211+
}
212+
213+
/// Result of filling a bitmask for the next token.
214+
public enum BitmaskResult: Equatable {
215+
/// Grammar is terminated or all tokens are blocked — generation should stop.
216+
case terminated
217+
/// All tokens are allowed — no mask needed, generate unconstrained.
218+
case unconstrained
219+
/// Bitmask was written — apply it to constrain sampling.
220+
case constrained
221+
}
222+
223+
/// Fill the bitmask directly into a caller-provided buffer (e.g., a GPU-visible MTLBuffer).
224+
///
225+
/// The caller must ensure the pointer has room for at least `(vocabularySize + 31) / 32`
226+
/// Int32 words.
227+
public mutating func fillBitmask(into pointer: UnsafeMutablePointer<Int32>) -> BitmaskResult {
228+
if isTerminated { return .terminated }
229+
230+
let needsApplication = matcher.fillNextTokenBitmask(pointer)
231+
if !needsApplication {
232+
// xgrammar signals all tokens are allowed — no mask needed
233+
return .unconstrained
234+
}
235+
// Check for all-zeros (no tokens allowed — grammar done)
236+
for i in 0..<bitmaskSize {
237+
if pointer[i] != 0 { return .constrained }
238+
}
239+
allTokensBlocked = true
240+
return .terminated
241+
}
196242
}
197243

198244
// MARK: - Float16 Masking

swift/Sources/CoreAILanguageModels/GuidedGeneration/XGrammarWrapper.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,29 @@ public final class GrammarMatcher {
136136
return xgrammar_matcher_is_terminated(handle)
137137
}
138138

139+
public var isCompleted: Bool {
140+
return xgrammar_matcher_is_completed(handle)
141+
}
142+
139143
public func reset() {
140144
xgrammar_matcher_reset(handle)
141145
}
146+
147+
@discardableResult
148+
public func rollback(_ numTokens: Int = 1) -> Bool {
149+
xgrammar_matcher_rollback(handle, Int32(numTokens))
150+
}
151+
152+
/// Returns the longest deterministic string from the current grammar state,
153+
/// or nil if no jump-forward is possible. Does not change matcher state.
154+
public func findJumpForwardString() -> String? {
155+
guard let cStr = xgrammar_matcher_find_jump_forward_string(handle) else {
156+
return nil
157+
}
158+
let result = String(cString: cStr)
159+
free(UnsafeMutablePointer(mutating: cStr))
160+
return result.isEmpty ? nil : result
161+
}
142162
}
143163

144164
// MARK: - Errors

swift/Sources/lib/CXGrammar/include/xgrammar_c_bridge.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ bool xgrammar_matcher_is_terminated(const XGrammarMatcher* matcher);
101101
// Reset matcher
102102
void xgrammar_matcher_reset(XGrammarMatcher* matcher);
103103

104+
// Rollback N tokens (restores grammar state to N tokens ago)
105+
bool xgrammar_matcher_rollback(XGrammarMatcher* matcher, int num_tokens);
106+
107+
// Find the longest deterministic string from the current grammar state.
108+
// Returns a malloc'd C string (caller must free), or NULL if no jump-forward is possible.
109+
const char* xgrammar_matcher_find_jump_forward_string(XGrammarMatcher* matcher);
110+
111+
// Check if the grammar's root rule has been fully matched (without requiring stop token)
112+
bool xgrammar_matcher_is_completed(const XGrammarMatcher* matcher);
113+
104114
// Free grammar matcher
105115
void xgrammar_matcher_free(XGrammarMatcher* matcher);
106116

swift/Sources/lib/CXGrammar/xgrammar_c_bridge.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,39 @@ void xgrammar_matcher_reset(XGrammarMatcher* matcher) {
222222
} catch (...) {}
223223
}
224224

225+
bool xgrammar_matcher_rollback(XGrammarMatcher* matcher, int num_tokens) {
226+
try {
227+
if (matcher) {
228+
matcher->cpp_obj.Rollback(num_tokens);
229+
return true;
230+
}
231+
} catch (...) {}
232+
return false;
233+
}
234+
235+
const char* xgrammar_matcher_find_jump_forward_string(XGrammarMatcher* matcher) {
236+
try {
237+
if (!matcher) return nullptr;
238+
std::string result = matcher->cpp_obj.FindJumpForwardString();
239+
if (result.empty()) return nullptr;
240+
char* buf = static_cast<char*>(malloc(result.size() + 1));
241+
if (!buf) return nullptr;
242+
memcpy(buf, result.c_str(), result.size() + 1);
243+
return buf;
244+
} catch (...) {
245+
return nullptr;
246+
}
247+
}
248+
249+
bool xgrammar_matcher_is_completed(const XGrammarMatcher* matcher) {
250+
try {
251+
if (!matcher) return false;
252+
return matcher->cpp_obj.IsCompleted();
253+
} catch (...) {
254+
return false;
255+
}
256+
}
257+
225258
void xgrammar_matcher_free(XGrammarMatcher* matcher) {
226259
delete matcher;
227260
}

swift/Tests/GuidedGenerationTests/ConstrainedGenerationSessionTests.swift

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,43 @@ struct ConstrainedGenerationSessionTests {
8484
#expect(allowedTokenIDs(session: &session).isEmpty)
8585
}
8686

87+
@Test func fillBitmaskIntoPointerMatchesNextTokenBitmask() throws {
88+
var session = try createTestSession()
89+
90+
// Get bitmask via the array-returning method
91+
let arrayBitmask = session.nextTokenBitmask()
92+
#expect(arrayBitmask != nil)
93+
94+
// Reset and get bitmask via the pointer method
95+
session.reset()
96+
let bitmaskSize = (session.vocabularySize + 31) / 32
97+
var pointerBitmask = [Int32](repeating: 0, count: bitmaskSize)
98+
let result = pointerBitmask.withUnsafeMutableBufferPointer { buf in
99+
session.fillBitmask(into: buf.baseAddress!)
100+
}
101+
#expect(result == .constrained)
102+
103+
// They should be identical
104+
#expect(arrayBitmask!.count == pointerBitmask.count)
105+
for i in 0..<arrayBitmask!.count {
106+
#expect(
107+
arrayBitmask![i] == pointerBitmask[i],
108+
"Mismatch at word \(i): array=\(arrayBitmask![i]) pointer=\(pointerBitmask[i])")
109+
}
110+
}
111+
112+
@Test func fillBitmaskReturnsTerminatedWhenDone() throws {
113+
var session = try createTestSession()
114+
driveToCompletion(session: &session)
115+
116+
let bitmaskSize = (session.vocabularySize + 31) / 32
117+
var buffer = [Int32](repeating: 0, count: bitmaskSize)
118+
let result = buffer.withUnsafeMutableBufferPointer { buf in
119+
session.fillBitmask(into: buf.baseAddress!)
120+
}
121+
#expect(result == .terminated)
122+
}
123+
87124
// MARK: - Token Acceptance Tests
88125

89126
@Test func acceptToken() throws {
@@ -109,6 +146,64 @@ struct ConstrainedGenerationSessionTests {
109146
)
110147
}
111148

149+
// MARK: - Rollback Tests
150+
151+
@Test func rollbackRestoresPriorState() throws {
152+
var session = try createTestSession()
153+
154+
let initialAllowed = allowedTokenIDs(session: &session)
155+
156+
// Accept "{" then rollback — should return to initial state
157+
_ = session.acceptToken(TestConstants.openBraceToken)
158+
let afterAccept = allowedTokenIDs(session: &session)
159+
#expect(afterAccept != initialAllowed)
160+
161+
let success = session.rollback(1)
162+
#expect(success, "rollback(1) should succeed")
163+
164+
let afterRollback = allowedTokenIDs(session: &session)
165+
#expect(afterRollback == initialAllowed, "After rollback, allowed tokens should match initial state")
166+
}
167+
168+
@Test func rollbackMultipleTokens() throws {
169+
var session = try createTestSession()
170+
171+
let initialAllowed = allowedTokenIDs(session: &session)
172+
173+
// Accept "{" then "\"" then rollback both
174+
_ = session.acceptToken(TestConstants.openBraceToken) // "{"
175+
_ = session.acceptToken(TestConstants.quoteToken) // "\""
176+
177+
let success = session.rollback(2)
178+
#expect(success)
179+
180+
let afterRollback = allowedTokenIDs(session: &session)
181+
#expect(afterRollback == initialAllowed)
182+
}
183+
184+
// MARK: - Jump Forward Tests
185+
186+
@Test func findJumpForwardStringReturnsNilAtChoice() throws {
187+
var session = try createTestSession()
188+
189+
// At the start, multiple tokens are valid ("{") — no deterministic jump
190+
let jump = session.findJumpForwardString()
191+
// The grammar may or may not have a deterministic prefix at the very start.
192+
// After "{", the next required token is "\"" (to start a key), but there could
193+
// be whitespace options. Just verify the API returns without crashing.
194+
_ = jump // no assertion on value — grammar-dependent
195+
}
196+
197+
@Test func findJumpForwardStringDoesNotMutateState() throws {
198+
var session = try createTestSession()
199+
200+
let beforeAllowed = allowedTokenIDs(session: &session)
201+
_ = session.findJumpForwardString()
202+
let afterAllowed = allowedTokenIDs(session: &session)
203+
204+
#expect(beforeAllowed == afterAllowed, "findJumpForwardString should not change grammar state")
205+
}
206+
112207
// MARK: - Reset Tests
113208

114209
@Test func reset() throws {

0 commit comments

Comments
 (0)