Skip to content

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

Merged
merged 2 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions Sources/Hub/Hub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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")) {
Copy link
Member

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 a processor, then the chat_template.json will be used.

I'm willing to diverge from this behaviour, given that:

  • Chat template divergence should not be expected (it's possibly a mistake if both templates differ)
  • Processor and tokenizer templates should be synced at some point.
  • There is no processor abstraction in swift-transformers.

cc @Rocketknight1 in case I'm missing some weird edge case.

// If chat_template.json exists and contains a chat_template field, use it to override the tokenizer_config
if let chatTemplate = chatTemplateConfig.chatTemplate?.stringValue {
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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,
Expand Down
37 changes: 37 additions & 0 deletions Tests/TokenizersTests/ChatTemplateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
XCTAssertTrue(qwen2VLEncoded == qwen2_5VLEncoded)
XCTAssertTrue(qwen2VLDecoded == qwen2_5VLDecoded && qwen2_5VLDecoded == expectedOutput)
XCTAssertEqual(qwen2VLEncoded, qwen2_5VLEncoded, "Encoded sequences should be equal")
XCTAssertEqual(qwen2VLDecoded, qwen2_5VLDecoded, "Decoded sequences should be equal")
XCTAssertEqual(qwen2_5VLDecoded, expectedOutput, "Decoded should match expected")

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)
Expand Down