Replies: 6 comments
|
This is intentional right now for multi-turn RL reasons. We're worried about token merging across message boundaries. I.e User: Then the tool is called, so the string becomes However, if tokenized as a whole, we're worried that the and the </tool:internet> and/or the strings could get merged, which would break token alignment and logprobs would be incorrect Instead of If <tool: internet> and are delimited by special tokens (guaranteed not merged by tokenizer), then we wouldn't have this problem, but if we want to support arbitrary end strings (#57), we can't ensure that. (plus, I figure that'll be a common use case, since adding a special token would require some fine-tuning to get off the ground). We don't really face this problem with single-turn things (we just tokenize the input and concatenate the output), but in multiturn cases, it becomes a question as to if you always concatenate (doing what we do), or detokenize and re-tokenize on every step, potentially running into the problems above. @YUki-666 who had a similar question. I'd love to hear your thoughts on this though. Do you have an alternative solution/believe this is a nonissue? I agree that this way of doing this looks nasty and breaks the nice property of |
|
Hmm, what would happen if we train our model with the current setup and then export it to HF and users in HF do the following for multi-turn prompts: Haven't we created a train/test breakdown? Right now, I think aligner/nemo adds some asserts (at least I think so, been a while since I looked at this) to catch token merging across turns. This might be annoying because it essentially means we have to pick a chat template that will be merged correctly, but in the long run it seems safer. Also, I think the token-merging is a problem only when we are working with new models from scratch(?). If we are working with instruct models with pre-defined tokenizers and chat_templates, the HF way of doing things should already work OOTB. |
|
Since it's common to use As for the boundary error, I'm a little bit confused now. (I can't understand well b/c maybe your <.bos> and <.eos> is hidden?)
|
|
I can see why message by message is useful (it makes masking easy). Can we however, throw an assertion that we should have On the other hand, it is best to keep implementation simple/consistent and restrict the types of tokens that signify the boundaries. For example -- why can we enforce that only single-token end-of-sequence is supported? or only single-token message-turn-end is supported? |
|
Maybe we can use custom chat_template to avoid it, but how we ensure that user will use our chat_template and apply it per message when inference? Still, I'd like to know why here is a boundary error, can someone give me an example? For example "{%- if tools %}\n {{- '<\|im_start\|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'Please reason step by step, and put your final answer within \\\\boxed{}.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool \| tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><\|im_end\|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<\|im_start\|>system\\n' + messages[0]['content'] + '<\|im_end\|>\\n' }}\n {%- else %}\n {{- '<\|im_start\|>system\\nPlease reason step by step, and put your final answer within \\\\boxed{}.<\|im_end\|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<\|im_start\|>' + message.role + '\\n' + message.content + '<\|im_end\|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<\|im_start\|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments \| tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<\|im_end\|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<\|im_start\|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<\|im_end\|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<\|im_start\|>assistant\\n' }}\n{%- endif %}\n"Convert it to a python script. (by AI) def format_chat_messages(messages, tools=None, add_generation_prompt=False):
"""Format chat messages with tools and special formatting.
Args:
messages: List of message dictionaries with 'role' and 'content' keys
tools: Optional list of tool definitions
add_generation_prompt: Whether to add an assistant prompt at the end
Returns:
Formatted string with all messages and tools
"""
result = []
# Handle system message and tools
if tools:
# Add system message
if messages and messages[0]['role'] == 'system':
result.append('<|im_start|>system\n' + messages[0]['content'])
else:
result.append('<|im_start|>system\nPlease reason step by step, and put your final answer within \\boxed{}.')
# Add tools section
result.append('\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\n'
'You are provided with function signatures within <tools></tools> XML tags:\n<tools>')
# Add each tool
for tool in tools:
result.append('\n' + str(tool))
result.append('\n</tools>\n\n'
'For each function call, return a json object with function name and arguments within '
'<tool_call></tool_call> XML tags:\n<tool_call>\n'
'{"name": <function-name>, "arguments": <args-json-object>}\n'
'</tool_call><|im_end|>\n')
else:
# Handle system message without tools
if messages and messages[0]['role'] == 'system':
result.append('<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n')
else:
result.append('<|im_start|>system\nPlease reason step by step, and put your final answer within \\boxed{}.<|im_end|>\n')
# Process all messages
for i, message in enumerate(messages):
role = message['role']
if role in ['user', 'system'] and (role != 'system' or i > 0):
# Handle user and non-first system messages
result.append(f'<|im_start|>{role}\n{message["content"]}<|im_end|>\n')
elif role == 'assistant':
# Handle assistant messages
result.append(f'<|im_start|>{role}')
if message.get('content'):
result.append('\n' + message['content'])
# Handle tool calls
if 'tool_calls' in message:
for tool_call in message['tool_calls']:
if 'function' in tool_call:
tool_call = tool_call['function']
result.append(f'\n<tool_call>\n{{"name": "{tool_call["name"]}", "arguments": {tool_call["arguments"]}}}\n</tool_call>')
result.append('<|im_end|>\n')
elif role == 'tool':
# Handle tool responses
if i == 0 or messages[i-1]['role'] != 'tool':
result.append('<|im_start|>user')
result.append(f'\n<tool_response>\n{message["content"]}\n</tool_response>')
if i == len(messages)-1 or messages[i+1]['role'] != 'tool':
result.append('<|im_end|>\n')
# Add generation prompt if requested
if add_generation_prompt:
result.append('<|im_start|>assistant\n')
return ''.join(result) |
If this is the case, then I think we should not do |
Uh oh!
There was an error while loading. Please reload this page.
Hi Team, I see here and here that the chat template is applied individually to each message. This is different from HF usage where the entire conversation list gets chat templated.
These two ways might not be compatible depending on the tokenizer.
All reactions