slice共享底层引起的“量子纠缠”

最新线上遇到一个比较诡异的现象,两个完全不同的对象,会存在“量子纠缠”,对象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拆分,然后按照拆分后的子数组下标分别加入到 fixMaskcontent 中,最后再通过拼接恢复成符合格式的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时,contentfixMask 如下:
在这里插入图片描述

处理完第二个token时,如下:
在这里插入图片描述

这里 fixMaskcontent 都是共享了第一个token, 当 content 添加元素 [4, 4] 时,会将原来的token:[1, 2, -1, 3, 3, -1, 5] 变为 [1, 2, -1, 3, 3, 4, 4], 因此 fixMask[1][0] 就会变成4。

那有没有什么办法避免呢?有的兄弟,有的。

我们只需要让 fixMaskcontent 不共享同一个底层slice就行了:

fixMask = append(fixMask, append([]int(nil), tt[i]...))
......
content = append(content, append([]int(nil), tt[i]...))

这里我们通过构造一个空的slice,然后将需要添加的元素append进去。虽然看起来很优雅,但这样有个不好的地方就是会触发多次slice的扩容。但这里只有在处理第一个token的时候才会进入这条分支逻辑,因此影响不是很大。

一句话总结:由于 fixMaskcontent 直接引用了原切片的子切片,导致底层数组共享;在后续 append 时修改了原数据,触发“常量不一致” panic。解决方法:首次填充时先 append([]int(nil), x...) 做一次拷贝即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值