最新线上遇到一个比较诡异的现象,两个完全不同的对象,会存在“量子纠缠”,对象A的写操作会引起对象B的改变。
需求大概是这样的,我需要处理语音大语言模型的输入,输入是一些按照固定格式组成的 []int
,中间会有一些特殊的token作为标识符,例如:
[AUDIO_START, 223423, 124124,AUDIO_END,
TEXT_START, 134234, 23989, TEXT_END,
PROMPT_START, PROMPT_AUDIO_TOKEN, PROMPT_END, AUDIO_SIGNAL]
这些特殊标识符长度不固定,但总是可以将其分为“常量+变量+常量+变量+…”的交替组合。而我这里需要的工作就是将这些输入合并到一起,所有的变量依次拼接,所有的常量只保留一份,一开始的想法是搜索这些常量的位置,将每个输入的变量提取出来。但后来模型越来越多,常量的格式也越来越多,如果依次查找这些常量未免太过复杂。因此,需要在输入的生成者端加上固定的标识符,来隔断常量与变量。
后来将输入变化为:
[AUDIO_START, -1, 223423, 124124, -1, AUDIO_END,
TEXT_START, -1, 134234, 23989, -1, TEXT_END,
PROMPT_START, PROMPT_AUDIO_TOKEN, PROMPT_END, AUDIO_SIGNAL]
这里在常量和变量的间隔之间加上特殊标识符号 -1
,一般模型的tokenizer不会使用这个数字,所以我可以很好的将变量和常量分割开来,因此代码就是:
func splitSliceByKey(slice []int, key int) [][]int {
var result [][]int
start := 0
for i, v := range slice {
if v == key {
result = append(result, slice[start:i])
start = i + 1
}
}
if start < len(slice) {
result = append(result, slice[start:])
}
return result
}
func equal(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func Merge(tokens [][]int) ([]int, error) {
if len(tokens) == 0 {
return nil, nil
}
var (
fixMask [][]int
content [][]int
isNil bool
)
isNil = true
for _, token := range tokens {
tt := splitSliceByKey(token, -1)
for i := 0; i < len(tt); i++ {
if i&1 == 0 {
// fixMask
if isNil {
fixMask = append(fixMask, tt[i])
} else {
if !equal(tt[i], fixMask[i>>1]) {
return nil, fmt.Errorf("inconsistent mask tokens")
}
}
} else {
// content
if isNil {
content = append(content, tt[i])
} else {
content[i>>1] = append(content[i>>1], tt[i]...)
}
}
}
isNil = false
}
var result []int
for len(fixMask) > 0 || len(content) > 0 {
if len(fixMask) > 0 {
result = append(result, fixMask[0]...)
fixMask = fixMask[1:]
}
if len(content) > 0 {
result = append(result, content[0]...)
content = content[1:]
}
}
return result, nil
}
这里的代码逻辑比较清晰,先根据 -1
将token拆分,然后按照拆分后的子数组下标分别加入到 fixMask
和 content
中,最后再通过拼接恢复成符合格式的token,并且,为了避免预期之外的问题,对每一条的token的常量是否相同做了一致性校验。
看起来没啥问题对吧,我们测试一个case:
func main() {
input := [][]int{
{1, 2, -1, 3, 3, -1, 5},
{1, 2, -1, 4, 4, -1, 5},
{1, 2, -1, 5, 5, -1, 5},
{1, 2, -1, 6, 6, -1, 5},
}
output, err := Merge(input)
if err != nil {
panic(err)
}
fmt.Println(output)
}
但是这里出现了预期之外的Panic:
完整的example参考这里 Go playground
panic: inconsistent mask tokens
WTF!这里的常量明明看起来是一样的啊,为啥会出现这个错误呢?一开始我想起了那个上午在地铁站上看的书籍《go语言精进之路》上有提及过关于for-range语法中用到的临时变量存在覆盖的问题,然后我尝试了下修改 Merge
函数,使用copy新构造一个切片:
for _, v := range tokens {
token := make([]int, len(v))
copy(token, v)
tt := splitSliceByKey(token, -1)
......
但是错误依然是存在的,没办法,只能单步调试看下了,结果发现了一个诡异的现象,就是当我运行至:
content[i>>1] = append(content[i>>1], tt[i]...)
fixMask
变量出现了变化!从 [[1,2],[5]]
变成了 [[1,2],[4]]
,看到这个现象我人都傻了。但是直觉告诉我,应该是有某个slice共享底层了:
当处理完第一个token时,content
和 fixMask
如下:
处理完第二个token时,如下:
这里 fixMask
和 content
都是共享了第一个token, 当 content
添加元素 [4, 4]
时,会将原来的token:[1, 2, -1, 3, 3, -1, 5]
变为 [1, 2, -1, 3, 3, 4, 4]
, 因此 fixMask[1][0]
就会变成4。
那有没有什么办法避免呢?有的兄弟,有的。
我们只需要让 fixMask
和 content
不共享同一个底层slice就行了:
fixMask = append(fixMask, append([]int(nil), tt[i]...))
......
content = append(content, append([]int(nil), tt[i]...))
这里我们通过构造一个空的slice,然后将需要添加的元素append进去。虽然看起来很优雅,但这样有个不好的地方就是会触发多次slice的扩容。但这里只有在处理第一个token的时候才会进入这条分支逻辑,因此影响不是很大。
一句话总结:由于 fixMask
与 content
直接引用了原切片的子切片,导致底层数组共享;在后续 append
时修改了原数据,触发“常量不一致” panic。解决方法:首次填充时先 append([]int(nil), x...)
做一次拷贝即可。