本文是关于文章 How do Language Models Bind Entities in Context? 的笔记.

Entity Binding, 即实体绑定, 是 LLM 在上下文中将属性与实体联系起来的能力. 例如, 如下情景中, AliceBob 分别与 lives in Parislives in Bangkok 属性绑定. LLM 根据绑定好的属性进一步回答随后的问题.

1
2
Context: Alice lives in the capital city of France. Bob lives in the capital city of Thailand.
Question: Which city does Bob live in?
阅读全文 »

本文是关于文章 The Linear Representation Hypothesis and the Geometry of Large Language Models 的笔记.

这篇文章的思想比较直白, 无外乎以下三点.

  • 每个概念(的出现与否)都被表达为(上下文/词)向量在一个子空间上的投影的取值.
  • 进一步地, 通过对这样的子空间使用线性的探针, 可以测量概念的出现.
  • 最后, 通过在该子空间上使用线性的一些操作, 可以干预概念的取值.
阅读全文 »

回顾资产定价之常识, 即资产未来(远期)价格折现值之期望为当前的合理价格. 若暂不考虑折现值, 则合理价格作为随机过程应当是一个鞅过程.

  1. 合理价格之估计本身也应当是一个鞅过程.
  2. 增加所考虑的市场上的信息时, 这一估计应当一致逼近合理价格.
  3. 这一估计之更新频率应当能与订单流频率相比拟.

由 bid-ask 价格直接平均所合成的 WAP/mid-price 都是不便之物, 不能足够好地作为合理价格的估计. 例如, WAP/mid-price 都没有其鞅性的理论依据; 前者容易受订单簿的噪声干扰, 后者则完全不考虑订单簿的订单量信息. 有必要在以上三个条件的约束下寻找新的合理价格的估计.

阅读全文 »

大语言模型因其卓越的性能吸引了大量的关注. 人们自然而然地会想问:

  • 它们内部是如何工作的(internal mechanics)
  • 我如何能让它们变得更好

通过回答第一个问题, 也许可以帮助我们回答第二个问题

阅读全文 »

以 DDPM 为代表的生成扩散模型在图像生成等领域上具有良好的效果, 并在学AI的文盲们面前小小地展示了一点数学功底的作用.

然而, 笔者读到的几乎所有扩散模型的教程都生硬地打着各种比喻, 提供了远超必须的直观性, 而完全失去了对其后数学动机的介绍. 即, 这样一个乍一看很陌生的过程是如何在保持数学上的有效性时被想到的.

因此, 本文希望展现这一想法可能的一种来龙去脉, 让读者感受到"想到这些关键点, 我也能发现 DDPM".

阅读全文 »

最近 LLMs (大语言模型)火得一比吊糟, 笔者也不能免俗, 因此弃坑光影包(并没有, 其实还在写)转战此处.

相比于一些连线性代数都没学好而每天阅读 transformer 基础教程的热衷于制造 fancy 字符串拼接轮子的前端程序员和勤于转发 10 个你必须知道的 prompt 技巧并喜欢制造 LLM 蜈蚣的产品经理[1], 笔者无意于在谈天狗屁通本体之外糊一层技术含量稀薄无比的壳. 模型参数量数据量大推理能力就是强, 上下文窗口大就是能碾压基于段落向量嵌入存取上下文的方法, 这是无论如何写 prompt 都改变不了的对比关系. 在价值链上, prompt 必将成为 CRUD 类似物, 也许可以养活一些 prompt boy, 但也可以一夜之间让他们被开掉; 一切不碰 LLM 权重的魔改套壳都不能从根本上提高 LLM 的能力.

训练 LLaMa 产生了大约 吨二氧化碳. 如此多的二氧化碳就已经是笔者买不起的了, 因此我等穷人似乎也只能去做 prompt boy. 一种不做 prompt boy 的可能的路径是转而像神经科学一样去解释 LLM 输出. 尽管关于可解释性的研究不能直接产出更强大或者更快的 LLM, 但说不定可以给出一些关于 LLM 的结构/初始化/训练过程/语料库构成如何影响 LLM 能力的 insight, 来提示我们如何改进 LLM 的能力, 或是在不损害能力的情况下排放更少的二氧化碳.

阅读全文 »

上回说到, 对世界的渲染被分包到了 WorldRenderer.render(MatrixStack ...) 这个函数. 这是一个将近 300 行的巨大函数, 再次展现了 ojng 招聘的员工素质.

992 行至 1005 行设置了 RenderSystem 内部的 shaderGameTime 值, 以备之后作为 uniform 上传给着色器; 对方块实体和实体渲染派发器进行了一些设置, 暂且略去不谈; 进行了一些光照更新, 这种更新只和游戏机制有关, 也不是我们所关心的.

1012 至 1023 行是某些关于调试视锥体的代码. 一般执行的分支是 1017 行的, 对正常的执行流毫无影响.

1025 至 1034 行是关于绘制天空和处理背景/雾效的代码. 鉴于正经光影包既不会用到原版天空也不会用到原版雾效, 我们也不关心这些代码的行为, 但作为我们碰到的第一段实际进行世界中物体的渲染的代码, 我们可以简单研究一下 renderSky 函数, 来挖掘 ojng 程序员惯用的编码模式.

以上是 WorldRenderer 的构造函数. 考虑到构造函数只执行一次, 而熟悉 OpenGL 的同学可能会认为渲染相关的逻辑需要每帧都执行, 函数体末尾三个 render*() 值得解释一下其作用. 以 renderStars() 函数为例:

容易看出, 这一函数的实际作用是填充了 this.starsBuffer 背后的顶点缓冲的内容.

通过搜索 starsBuffer 的用法, 可以发现正是 public void renderSky(MatrixStack...) 函数执行了星星和天空的绘制:

1630 行的 this.lightSkyBuffer.setShader(...) 看似只是进行了类似于 glUseProgram 的操作, 但实际上同时进行了绘制(glDrawElements), 惊不惊喜意不意外? yarn 有的方法命名还是比较有问题的 在后面的 getFogColorOverride 返回非空的东西时[1], ojng 当场造了个 buffer, 用 BufferRender 来现场渲染. 要复用这种不太变的 buffer 就坚持到底嘛.

此处还出现了一个在源码中很常见的类: BufferBuilder. 参考 private void renderStars(BufferBuilder buffer) 函数, 可以得到 BufferBuilder 的主要使用模式:

BufferBuilder 类本质上是对 ByteBuffer 的一个包装, 同时也是多个顶点数据和相关绘制参数构成的栈.[2]

  • bufferBuilder.begin() 可以指定绘制模式和顶点格式, 并将 bufferBuilder 置于准备接受新一个几何体的顶点数据的状态.
  • bufferBuilder.vertex(x, y, z) 会将 x,y,z 的值按顺序写入内置的 ByteBuffer, 并将内部状态设置为对下一个元素写入
  • bufferBuilder.next() 则会告知 bufferBuilder 完成对当前顶点的写入, 适当调整内部 ByteBuffer 的容量, 并准备好写入下一个顶点.
  • bufferBuilder.end() 则结束对当前顶点数据的收集, 根据一定条件补充索引数据(即供 OpenGL 使用的 index buffer), 并将收集到的绘制参数(例如顶点数、绘制模式、顶点格式等)打包, 压入 bufferBuilder.parameters 中, 随后重置 bufferBuilder 的状态.
  • vertexBuffer.upload(bufferBuilder) 则会弹出栈顶的数据, 并上传到其对应的 OpenGL 顶点缓冲区中.
  • bufferBuilder 在某些条件下还会将几何面按照离摄像机从近到远的顺序排序, 这是为了充分利用显卡 early-z 的特性, 减少过度绘制.

回到对 WorldRenderer.render 的分析上来.

1036 和 1038 行的代码是我们啃屎山的开始.

屎山: setupTerrain

WorldRenderer.render 中的第一座屎山叫 setupTerrain. 建议读者直接跳转到本节末尾阅读结论.

setupTerrain 函数首先检查了相比上一帧, 摄影机是否移动到了另一个 ChunkSection. ChunkSection 就是 大小的那个被一般玩家叫做区块的东西, 但 ojng 叫它 ChunkSection. 如果发生了移动, 则调用 BuiltChunkStorage.updateCameraPosition(x, z) 来对部分区块设置其 origin 成员(仿佛结果是 xz 的某种阶梯函数):[3]

随后通过调用 ChunkBuilder.setCameraPosition() 告知 chunkBuilder 摄像机的新位置. 这是为了帮助 chunkBuilder 进行诸如将区块按离摄像机的距离来排序的操作.

此函数中涉及到了多个 yarn 中未被赋予名字的变量和方法, 不妨在此处给出其 mojang 名以供参考:

obfuscated/yarn name type mojang name
WorldRenderer - net.minecraft.client.renderer.LevelRenderer
ChunkInfoList - LevelRendererRenderChunkStorage
field_34808 Future lastFullRenderChunkUpdate
field_34809 AtomicBoolean needsFrustumUpdate
field_34810 boolean needsFullRenderChunkUpdate
field_34811 AtomicLong nextFullUpdateMillis
field_34817 AtomicReference renderChunkStorage
field_34818 WorldRenderer.ChunkInfoList renderInfoMap
field_34819 LinkedHashSet renderChunks
method_34808 void method_34808(LinkedHashSet, ChunkInfoList, Vec3d, Queue, boolean); updateRenderChunks
method_38549 private void method_38549(Camera, Queue); initializeQueueForFullUpdate

如果相机相比上一帧移到了不同的 空间, 或者 needsFullRenderChunkUpdate 标记被设置时, 则进行一些与区块遮挡剔除相关的操作. 如果没有因为调试而固定使用某个此前捕获的视锥体来进行剔除 (!hasForcedFrustum), 且 lastFullRenderChunkUpdate 为空或者不是正在进行的, 则将以下操作提交到 MAIN_WORKER_EXECUTOR 线程上执行:

  • 初始化某个广度优先的 FIFO 队列, 即方法 initializeQueueForFullUpdate 的内容: 如果当前摄像机所在区块已经被渲染(可能需要换个词, 反正就是 this.chunks.getRenderedChunk(blockPos)!=null), 则将当前区块对应的 ChunkInfo 加入队列以作为广度优先的开始; 否则将视距内某个高度上所有已渲染的区块对应的 ChunkInfo 按从近到远的顺序加入该队列
    801
    802
    ArrayDeque<ChunkInfo> queue = Queues.newArrayDeque();
    this.method_38549(camera, queue);
  • 进行某种奇怪的广度优先操作, 即方法 updateRenderChunks 的内容: 从队列中取出一个 ChunkInfo, 将其放入 field_34817.get().field_34819, 并对其 6 个方向上邻接的区块进行某些神必的判断(可能是为了剔除掉例如 6 个方向上都被遮挡的区块), 更新其剔除状态(updateCullingState), 并计算其 propagationLevel 以便于调试的可视化; 最后在某些情况下将邻接区块放回队列, 并更新 field_34817.get().field_34818 对于该区块的值; 有时还把 WorldRenderer.field_34811 设置到 500 毫秒以后, 以规划一次完整的遮挡剔除运算
    803
    804
    class_6600 lv = new class_6600(this.chunks.chunks.length);
    this.method_34808(lv.field_34819, lv.field_34818, vec3d, queue, bl2);
  • 最后将以上结果填充到 field_34817 中, 并设置 field_34809 以表明有新的剔除结果可用.

提交以上任务后, 渲染线程立即调用 field_34817.get(), 以得到此前已有的任务结果. 如果 builtChunks 非空, 则从 builtChunks 选取 field_34817.get().field_34818 中存在的区块加入到另一个队列中, 然后再重复 method_34808 的操作.

最后, 在转动视角或有新鲜的剔除结果(field_34809 被设置)时, 通过调用 WorldRenderer.applyFrustum(), 将 field_34817 内所有包围盒与视锥相交或者在视锥体内的区块加入 WorldRenderer.chunkInfos 中.

总的来看, setupTerrain 函数的主要功能是对区块进行遮挡剔除和视锥剔除; 如此复杂的函数笔者不想用心读, 写出来的东西读者也不忍心看, 想必 ojng 的程序员也不想用心维护, 难免有几个 bug .[4]

屎山: updateChunks

上一座屎山 setupTerrain 函数过滤过的区块被加入了 WorldRenderer.chunkInfos, 因此这座屎山就继续对 WorldRenderer.chunkInfos 的内容进行遍历. 只有在某个区块既带有 needsRebuild 标记(即满足 builtChunk.needsRebuild())且 WorldRenderer.world.getChunk(chunkPos.x, chunkPos.z).shouldRenderOnUpdate() 时, 才会继续对其处理. 对于被处理的区块, 以下行为将发生:

  • 在客户端选项里的 chunkBuilderMode 是邻近(ChunkBuilderMode.NEARBY)时, 带有 needsImportantRebuild 标记或者离摄像机距离小于 的区块会在渲染线程中立即被同步地重建(rebuild).
  • 在客户端选项里的 chunkBuilderMode 是受玩家影响的(ChunkBuilderMode.PLAYER_AFFECTED)时, 只有带有 needsImportantRebuild 标记的区块会立即被同步重建.
  • 以上的同步重建完成后会清除 needsRebuild 标记和 needsImportantRebuild 标记, 以避免重建重复发生; 重建完成时一个上传重建结果至显存的任务将被添加到 WorldRenderer.chunkBuilder 内部的上传队列 uploadQueue
  • WorldRenderer.chunkInfos 完成遍历后, uploadQueue 将被执行直至清空.
  • 不满足以上重建条件的区块会被收集起来, 丢进 WorldRenderer.chunkBuilder 内部的重建队列 prioritizedTaskQueuetaskQueue 里异步重建, 重建完成时上传重建结果的任务同样将被添加到上传队列; 在提交重建任务后, 重建相关标记也会被清除.

区块重建任务最终会调用 ChunkBuilder.BuiltChunk.RebuildTask.render 函数, 以收集方块/方块实体的信息, 构建区块的顶点数据, 并写入不同的 RenderLayer 的缓冲区中:[5]

在完成区块剔除/区块更新和上传顶点缓冲区后, 各 RenderLayer 对应的顶点缓冲区就准备好绘制了. WorldRenderer.renderLayer(RenderLayer, MatrixStack, double, double, double, Matrix4f) 是具体执行此类绘制的函数. 欲知后事如何, 且听下回分解.


  1. 1.似乎是渲染日出和日落的时候覆盖掉大气雾时使用的网格 ↩︎
  2. 2.szszss的博文中对常常与BufferBuilder一起出现的Tessellator的来源进行了解释,即试图模拟OpenGL立即模式的惯用法. ↩︎
  3. 3.剧透一下,origin成员会与相机位置进行加减之后作为chunkOffset这一uniform变量传递给着色器,并加到顶点的位置上来获得真正的世界坐标. ↩︎
  4. 4.一般的实践是利用上一帧的深度缓冲,与各区块的包围盒的深度进行比较,bug少不说,程序员写着也舒服. ↩︎
  5. 5.RenderLayer可以理解成需要不同的渲染方式的物体构成的集合,例如SOLID是完全不透明的方块,CUTOUT是有镂空部分的方块,CUTOUT_MIPPED是带有mipmapping的前者,TRANSLUCENT是半透明的方块.详见这里. ↩︎

上回说到, 渲染一帧(包括绘制世界和绘制UI)的主要逻辑发生在 net/minecraft/client/render/GameRenderer.java 中. 而实际上, GameRenderer.java 又将渲染世界的任务分包给了同一目录下的 WorldRenderer.java.

GameRenderer.render 函数完成的主要工作罗列如下:

阅读全文 »
0%