-
Notifications
You must be signed in to change notification settings - Fork 131
Prefer chat_template.json for chat template #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -177,7 +177,7 @@ public class LanguageModelConfigurationFromHub { | |
modelName: String, | ||
hubApi: HubApi = .shared | ||
) async throws -> Configurations { | ||
let filesToDownload = ["config.json", "tokenizer_config.json", "tokenizer.json"] | ||
let filesToDownload = ["config.json", "tokenizer_config.json", "chat_template.json", "tokenizer.json"] | ||
let repo = Hub.Repo(id: modelName) | ||
let downloadedModelFolder = try await hubApi.snapshot(from: repo, matching: filesToDownload) | ||
|
||
|
@@ -190,9 +190,32 @@ public class LanguageModelConfigurationFromHub { | |
) async throws -> Configurations { | ||
// Note tokenizerConfig may be nil (does not exist in all models) | ||
let modelConfig = try hubApi.configuration(fileURL: modelFolder.appending(path: "config.json")) | ||
// First try to get the tokenizer_config.json | ||
let tokenizerConfig = try? hubApi.configuration(fileURL: modelFolder.appending(path: "tokenizer_config.json")) | ||
// Check for chat_template.json, which contains the preferred chat template for vision language models | ||
if let chatTemplateConfig = try? hubApi.configuration(fileURL: modelFolder.appending(path: "chat_template.json")) { | ||
// If chat_template.json exists and contains a chat_template field, use it to override the tokenizer_config | ||
if let chatTemplate = chatTemplateConfig.chatTemplate?.stringValue { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, this could potentially be an array too. But this is discouraged. |
||
var updatedConfig: Config | ||
if var configDict = tokenizerConfig?.dictionary { | ||
// Override the chat template in the existing tokenizer config | ||
configDict["chat_template"] = chatTemplate | ||
updatedConfig = Config(configDict) | ||
} else { | ||
// Create a new config with just the chat template | ||
updatedConfig = Config(["chat_template": chatTemplate]) | ||
} | ||
let tokenizerVocab = try hubApi.configuration(fileURL: modelFolder.appending(path: "tokenizer.json")) | ||
let configs = Configurations( | ||
modelConfig: modelConfig, | ||
tokenizerConfig: updatedConfig, | ||
tokenizerData: tokenizerVocab | ||
) | ||
return configs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a fan of repeating this code block and the return here, inside the nested ifs. I'd recommend we write a helper function to potentially update the chat template, such as: func updatedTokenizerConfig(tokenizerConfig: Config?, chatTemplateConfig: Config?) -> Config? {
guard
let chatTemplateConfig = chatTemplateConfig,
let overrideChatTemplate = chatTemplateConfig.chatTemplate?.stringValue else {
return tokenizerConfig
}
var configDict = tokenizerConfig?.dictionary ?? [:]
configDict["chat_template"] = overrideChatTemplate
return Config(configDict)
} And then we can just use this before the return: let configs = Configurations(
modelConfig: modelConfig,
tokenizerConfig: updatedTokenizerConfig(tokenizerConfig: tokenizerConfig, chatTemplateConfig: chatTemplateConfig),
tokenizerData: tokenizerVocab
) |
||
} | ||
} | ||
// If chat_template.json doesn't exist or doesn't have a chat_template field, use the tokenizer_config as is | ||
let tokenizerVocab = try hubApi.configuration(fileURL: modelFolder.appending(path: "tokenizer.json")) | ||
|
||
let configs = Configurations( | ||
modelConfig: modelConfig, | ||
tokenizerConfig: tokenizerConfig, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -178,6 +178,43 @@ What is the weather in Paris today?<|im_end|> | |||||||||||
XCTAssertTrue(tokenizer.hasChatTemplate) | ||||||||||||
} | ||||||||||||
|
||||||||||||
// Test for vision models with a vision chat template in chat_template.json | ||||||||||||
func testChatTemplateFromChatTemplateJson() async throws { | ||||||||||||
let visionMessages = [ | ||||||||||||
[ | ||||||||||||
"role": "user", | ||||||||||||
"content": [ | ||||||||||||
[ | ||||||||||||
"type": "text", | ||||||||||||
"text": "What's in this image?", | ||||||||||||
] as [String: String], | ||||||||||||
[ | ||||||||||||
"type": "image", | ||||||||||||
"image_url": "example.jpg", | ||||||||||||
] as [String: String], | ||||||||||||
] as [[String: String]], | ||||||||||||
] as [String: Any] | ||||||||||||
] as [[String: Any]] | ||||||||||||
// Qwen 2 VL does not have a chat_template.json file. The chat template is in tokenizer_config.json. | ||||||||||||
let qwen2VLTokenizer = try await AutoTokenizer.from(pretrained: "mlx-community/Qwen2-VL-7B-Instruct-4bit") | ||||||||||||
// Qwen 2.5 VL has a chat_template.json file with a different chat template than the one in tokenizer_config.json. | ||||||||||||
let qwen2_5VLTokenizer = try await AutoTokenizer.from(pretrained: "mlx-community/Qwen2.5-VL-7B-Instruct-4bit") | ||||||||||||
let qwen2VLEncoded = try qwen2VLTokenizer.applyChatTemplate(messages: visionMessages) | ||||||||||||
let qwen2VLDecoded = qwen2VLTokenizer.decode(tokens: qwen2VLEncoded) | ||||||||||||
let qwen2_5VLEncoded = try qwen2_5VLTokenizer.applyChatTemplate(messages: visionMessages) | ||||||||||||
let qwen2_5VLDecoded = qwen2_5VLTokenizer.decode(tokens: qwen2_5VLEncoded) | ||||||||||||
let expectedOutput = """ | ||||||||||||
<|im_start|>system | ||||||||||||
You are a helpful assistant.<|im_end|> | ||||||||||||
<|im_start|>user | ||||||||||||
What's in this image?<|vision_start|><|image_pad|><|vision_end|><|im_end|> | ||||||||||||
<|im_start|>assistant | ||||||||||||
|
||||||||||||
""" | ||||||||||||
XCTAssertTrue(qwen2VLEncoded == qwen2_5VLEncoded) | ||||||||||||
XCTAssertTrue(qwen2VLDecoded == qwen2_5VLDecoded && qwen2_5VLDecoded == expectedOutput) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
nit, should provide better error messages maybe |
||||||||||||
} | ||||||||||||
|
||||||||||||
func testApplyTemplateError() async throws { | ||||||||||||
let tokenizer = try await AutoTokenizer.from(pretrained: "google-bert/bert-base-uncased") | ||||||||||||
XCTAssertFalse(tokenizer.hasChatTemplate) | ||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, this is not the same algorithm used in transformers. IIRC, if we instantiate a tokenizer from a repo where the tokenizer has a chat template and a different
chat_template.json
, the template from the tokenizer will still be used. However, if we instantiate aprocessor
, then thechat_template.json
will be used.I'm willing to diverge from this behaviour, given that:
processor
abstraction in swift-transformers.cc @Rocketknight1 in case I'm missing some weird edge case.