admin

  • 通史阅读:魏晋南北朝

    ·

    很长时间以来,历史都是我的盲区,而“魏晋南北朝”大概属于盲区中的盲区。由于各种原因,大概大家并不太愿意过多的去讲述这段历史,相关的影视作品、游戏等内容也比较少,在大众的认识中,存在感是相对更低的。

    从哪里开始呢?就从那场改变曹氏与司马氏权力格局的政变开始吧。

    自249年“高平陵之变”,司马家族从曹魏手中夺权,最终在265年,魏元帝曹奂“禅”位司马炎,建立“晋”,是为魏武帝。316年,晋愍帝出降前赵刘曜。318年,琅琊王司马睿在建康继帝位,是为晋元帝。最后,在420年,晋恭帝退位,刘裕登基建“宋”(多称为“刘宋”)。南朝分别历经宋、齐、梁、陈,陈最后为隋所灭。

    而北方也非常混乱,各种政权势力繁多,包括刘渊、刘曜、石勒、石闵、慕容儁(音“骏”)、苻坚、拓跋珪,最终由拓跋珪建立的北魏逐步统一北方。约在398年,拓跋珪迁都平城,即皇帝位。北魏在传续百年后,分裂为宇文泰集团、高欢集团,分别继承部分北魏“遗产”,建立北周、北齐。北齐早期虽然更强,但北周历经七十年的“图治”,最终灭北齐(约577年)。但,并没有过太久,在581年,杨坚废周静帝自立(参考:隋文帝),改国号为隋,北周灭亡。

    最后由北方的“隋”继续征战南北,统一全国。

    1. 混乱、短暂的西晋

    自司马炎称帝,到愍帝出降,西晋总计存续约五十年。西晋除了比较短暂外,另一个存在感很低的原因大概是:“混乱”,后世用“八王之乱”概括之。

    八王之乱,是一场司马家族内部“混乱”的权力之争。晋武帝司马炎去世后,传位给愚弱的惠帝司马衷,而后皇后贾南风(西晋开国之臣贾充之女)当权。先后利用多方力量分别杀杨骏(惠帝母杨氏族),而后汝南王司马亮被杀、楚王司马玮亦被杀,最后,甚至杀了太子司马遹。这激怒了其他诸王,也给其他诸王以理由。之后,赵王司马伦联合齐王司马囧,尽灭贾后及其党羽。

    接着赵王伦称帝,诸王自然不认可,于是开启了“八王之乱”的下半场。不久,赵王伦被齐王等所诛杀,惠帝虽“愚”,再次复位。之后,由于诸王专权的问题,齐王、成都王、河间王、长沙王、东海王均卷入这场“混乱”之中。

    东海王司马越站到了最后,但他并没有“得意”多久。北方匈奴部逐渐强大,而内部怀帝(惠帝死后,他的弟弟豫章王即位是为怀帝)因惧其专权,永嘉五年(311年),“发布司马越的罪状,要各方镇讨伐”司马越。司马越率领着西晋最后的十万大军,自己在忧愤中死去,而其大军也被匈奴部的石勒所灭。自此,西晋便更加无力抵抗北方匈奴部。五年后,晋愍帝窘迫出降于刘曜。西晋灭亡。

    2. 北方诸族(五胡十六国)

    “五胡十六国” 通常可以以刘渊称帝(308年),建立“汉”为初始。刘渊为匈奴五部大单于,在八王之乱期间成都王司马颖,曾极力拉拢,在司马颖败落后,便联合匈奴各部,建立自己的政权。帝位传至其孙刘粲(音“灿”)后,粲被靳准所杀,靳准曾向西晋称臣;而后,靳准被石勒、刘曜所败,石勒、刘曜均是刘聪(刘渊子)的临终时的顾命大臣。“靳准之乱”后,刘曜、石勒也各自为政,分别建立了“前赵”、“后赵”。

    2.1 后赵

    这一时期的“国家”、“帝王”都很短暂与混乱。这里看看出生卑微的石勒建立后赵。

    329年石勒灭前赵(刘曜),次年称帝;333年石勒卒,次年其从子石虎篡位;349年石虎卒,其子为争帝位互相残杀。次年(350年),石虎养孙冉闵自立为帝,改国号魏,史称冉魏。后来,石氏子孙投降东晋,也被杀及诛灭。

    2.2 群雄并立

    这段时间,在北方建立政权的先后还有:慕容氏建立的前燕、后燕、南燕、西燕、后凉、后秦、西秦、北魏,这些势力多数时候是并立存在。曾经一度“北方的八个势力,并立了九年之久”(傅 “中国通史”)。

    2.3 拓跋氏统一北方

    直到,北魏拓跋氏一统北方,北方大地才再次有了一段相对和平的时代。北魏开国皇帝拓跋珪,于398年称帝,建都平城(即大同)。

    3. 摇摇欲坠的东晋

    自318年,晋元帝司马睿在建康即帝位,到420年刘宋建立。虽由一百年时间,但是,期间司马氏从不曾真正较长时间掌握大权,一直都是在“门阀”、“权臣”的交替与平衡中生存。

    先是“王与马共天下”,而后王敦起兵,晋元帝“忧愤而死”,翻译成今天的话,大概就是被气死了。那个说出“举目见日不见长安”的晋明帝虽平定了王敦之乱,但奈何命短,在位三年后去世。再到晋成帝则是苏峻之乱,一度破建康城,劫成帝。

    而后,桓温崛起。桓温先是北伐,后谋求自立为帝未果。桓温死后,其家族依旧强大,其子桓玄袭其位,其弟、侄等任荆州刺史、扬州刺史、江州刺史等要职。

    桓玄一度曾称帝,最终被刘裕平定,还帝位于晋室。哦,短暂的归还。

    4. 东晋北伐的困局

    很多人大概都听过“闻鸡起舞”这个成语,也还有不少人知道祖逖艰难北伐的故事。但似乎很少人谈论,为什么西晋北伐几乎是不可能成功的。祖逖、桓温、刘裕都是北伐名将,他们的经历大概某种程度给出了回答。

    4.1 闻鸡起舞的祖逖

    当祖逖在“进”表要北伐时(当时北方石勒、刘曜混战),晋室元帝司马睿当然口头上是非常支持的,但并给出不太大的实质上的支持:“以逖为奋威将军、豫州刺史,给千人禀,布三千匹,不给铠仗,使自招募”。

    时帝方拓定江南,未遑北伐,逖进说曰:“晋室之乱,非上无道而下怨叛也。由籓王争权,自相诛灭,遂使戎狄乘隙,毒流中原。今遗黎既被残酷,人有奋击之志。大王诚能发威命将,使若逖等为之统主,则郡国豪杰必因风向赴,沈弱之士欣于来苏,庶几国耻可雪,愿大王图之。”帝乃以逖为奋威将军、豫州刺史,给千人禀,布三千匹,不给铠仗,使自招募。

    晋书/卷062

    而祖逖也算不负众望,一路北伐恢复了大量黄河以南的土地,并与北方的石勒隔“河”相持对峙。

    而后,321年,晋元帝司马睿派遣戴渊(戴若思)为征西将军。祖逖有自己的战略考量,对戴若思并不太认可,同时也看到朝廷内政不稳,最终在“忧愤”与不得志中去世。关于这一段,各位看客可以读读唐朝房玄龄所著《晋书》中祖逖的列传内容:

    石勒不敢窥兵河南,使成皋县修逖母墓,因与逖书,求通使交市,逖不报书,而听互市,收利十倍,于是公私丰赡,士马日滋。方当推锋越河,扫清冀朔,会朝廷将遣戴若思为都督,逖以若思是吴人,虽有才望,无弘致远识,且已翦荆棘,收河南地,而若思雍容,一旦来统之,意甚怏怏。且闻王敦与刘隗等构隙,虑有内难,大功不遂。感激发病,乃致妻孥汝南大木山下。时中原士庶咸谓逖当进据武牢,而反置家险厄,或谏之,不纳。逖虽内怀忧愤,而图进取不辍,营缮武牢城,城北临黄河,西接成皋,四望甚远。逖恐南无坚垒,必为贼所袭,乃使从子汝南太守济率汝阳太守张敞、新蔡内史周闳率众筑垒。未成,而逖病甚。先是,华谭、庾阐问术人戴洋,洋曰:“祖豫州九月当死。”初有妖星见于豫州之分,历阳陈训又谓人曰:“今年西北大将当死。”逖亦见星,曰:“为我矣!方平河北,而天欲杀我,此乃不祐国也。”俄卒于雍丘,时年五十六。豫州士女若丧考妣,谯梁百姓为之立祠。册赠车骑将军。王敦久怀逆乱,畏逖不敢发,至是始得肆意焉。寻以逖弟约代领其众。约别有传。逖兄纳。

    晋书/卷062

    为什么建议要读一下这一段内容呢?我们现在看到的“通俗”内容中关于祖逖的说明,大多都有一些差异,而这些内容多半是出自上述原始材料,读者不同,翻译不同,理解也可能有不同。所以,大可读读原始材料。

    这段内容其实是可以有两种解读的。一方面祖逖北伐是比较成功的,收复和稳定了大量黄河以南的失地。此时,恰逢重新积蓄力量,修整军队的一段略微平静些的时间。石勒、祖逖也暂时的“默契休战”。石勒“修逖母墓”、“求通市”,祖逖虽未回复,但听任之,并且在互市过程中,获利颇丰(收利十倍),从而兵马日渐强壮(“士马日滋”)。这时,祖逖继续加强城池修缮,并且内心并不认可朝廷派遣来的“都督戴若思”。

    也许祖逖的战略是百分百正确,但祖逖的行为在政治上是“危险”的。只是恰逢东晋皇室羸弱,“门阀掌权”、权臣王敦亦有异心,并且祖逖不久就死去。否则,局势虽不同,但也可能会面临类似于“岳飞”类似的境地。

    4.2 北伐的困局

    可以看到,东晋朝廷虽然表面支持祖逖北伐,但并未给予实质性支持,某种程度上甚至是略有掣肘,尤其是在祖逖北伐顺利的时候。在傅乐成中国通史中有如下描述,则道出了北伐真正的困境:

    当时除少数苟安的士大夫外,晋人莫不希望早复中原,他们普遍抱有一种思想,认为谁能驱逐胡虏,谁便有称帝的资格,这种思想在东晋中末期尤为显著。因此若干野心家,都思北伐立功,以求名正言顺的称帝。正因如此,晋室中央及其执政者渐至视外战为畏途,不但不加支持,反处处掣肘。

    傅乐成 《中国通史》

    结合着祖逖、桓温、刘裕的北伐和后面的历史,可以看到,上面这段话是揭露了北伐困局的真相的。对于将相北伐风险、挑战自然是非常高的,并且一定会受到朝廷猜忌与防范,所以,如果一个人只是想北伐、恢复晋室,那么他可能北伐能够成功,但他不可能成功。最后,就“催生”了桓温、刘裕的“模式”,北伐成功,军队强大,只能自己做自己的皇帝。

    在中国古代,正如谚语所云“太平本是将军定,不许将军见太平”。

    4.3 桓温

    桓温家族在东晋虽“门地不高”,但也是“士族”阶层(参考)。桓温在多方势力“平衡”之中,镇守荆州,而在当时荆州是南方的一个重镇。而后,凭借勇武与敏锐的军事能力,“平灭成汉”,很大程度上扩大了东晋在南方的势力范围,桓温也因此“声名大盛”。而后,桓温多次北伐,其中第二次北伐曾成功收复洛阳。到桓温“晚年”,则主要谋求在朝廷的最高权利,但因为各种原因与阻碍,最终未能代晋称帝。

    桓温的两次北伐,都因粮尽退兵,为敌所乘,而致大败,可知桓温是以孤军击敌,并无后援。晋室中央绝不与他合作,必使他失败而后快,因为假使桓温一旦成功,晋室也将不保。事实上桓温确有做皇帝的念头,他准备平燕归来后便篡位的。所以就晋室的立场来说,它之不协助桓温北伐,也自有其苦衷,但复兴大业,却因而断送。

    傅乐成 《中国通史》

    4.4 刘裕

    所以,东晋北伐为什么总难成功呢?究其根本原因,大概是,力量难以统一。东晋王室是有这个威望的,但此时的司马家族已经势弱,虽能够聚集起一些力量,但是无法掌控这些力量。

    而其他力量,虽然能够在一段时间、或者某些地方称霸,甚至能够给北方诸族以致命打击,但是这些力量也因为孤立无援、甚至“黄雀在后”而无法完成“北伐”。

    但最后刘裕做到了,或者某种程度做到了。他曾经击败篡位的桓玄(桓温嗣子)恢复晋室、北伐克复洛阳与长安(虽然很短暂)、平定多次叛乱、打败成都王等。北方最终由于,后方不稳定,而很快再次丢失。

    最后,不出意外,意外就发生了。420年,刘裕迫使晋帝禅让,即位建立“宋”。

    5. 前秦与淝水之战

    在南北朝时期,“淝水之战”是关键的一战。北方原本强大的“前秦”(苻坚),则因为这一战而快速陨落。之前,苻坚所打败的一众力量则再次纷纷独立;南方晋室因为这一战的胜利,暂且保住了地盘。而由谢氏建立的北府兵也成长起来。在后来,北府兵的力量非常大程度的限制了桓氏(桓温家族)的力量。

    但,最终,北府兵旧将刘裕结束了东晋王朝。建立“刘宋”。

    6. 北朝

    “北朝”大概分成了两个阶段,一个是“群雄”争霸的五胡十六国时期,另一个是北魏(包括北周和北齐)时期。

    北魏由拓跋珪建立,最初定都“平城”(山西大同),而后拓跋焘统一北方。之后孝文帝迁都洛阳,并大力推行汉化,包括使用汉人姓氏、通婚、文字语言等都使用汉文。

    而后,北魏延续约100年,后分裂为东、西魏,并很快由宇文泰、高欢分别建立北周、北齐,两国之间多有摩擦与交战。起初,北齐虽占上风,但最终更加锐意变革的北周最终打败北齐。

    但,之后的北周并没有延续很久,最终,皇权由建立隋朝的杨坚所夺。

    不过,北周、隋、唐之间都有非常近的亲戚关系。例如杨坚的一个女儿,就是北周宣帝的皇后;更不用说,李渊和杨广就是姨表亲。

    7. 时代诗文

    7.1 举目见日 不见长安

    高中时就读过《举目见日 不见长安》这段内容,印象是非常深刻的,但是那种“莫名其妙”感记得也非常清楚。高中时,完全不了解东晋的情况,不知道晋元帝的困局,不知道东晋面临北方外族的南侵,不知道内部政局的混乱,自然难以理解这篇内容。

    明皇帝讳绍,字道畿,元皇帝长子也。幼而聪哲,为元帝所宠异。年数岁,尝坐置膝前,属长安使来,因问帝曰:「汝谓日与长安孰远?」对曰:「长安近。不闻人从日边来,居然可知也。」元帝异之。明日,宴群僚,又问之。对曰:「日近。」元帝失色,曰:「何乃异间者之言乎?」对曰:「举目则见日,不见长安。」由是益奇之。

    晋书 明帝

    再读这段文字,就有了不一样的体会。

    元帝在次日的“聚会”上,故意再次提问,可以看到一个普通的父亲的“虚荣心”,想在群僚面前让自己的孩子表现一下,就像现在的家长让自己的孩子在大人面前背诵一首唐诗、唱一首歌曲、表演一小段舞蹈、弹奏一首曲子是一样的。

    另外,晋元帝虽在南方逐步站稳,但是可以说是内外交困。外部北方政权(如石勒)一旦稳固则经常南侵;内部一方面内政士族强大,外部藩镇(王敦等)也不受指挥。此时,如果下一代如有明主,那么长远来看“东晋”政权则可能逐步强大。晋元帝对于下一代的期望自然相比于普通家长更加殷切。

    7.2 北朝名歌 敕勒歌

    敕勒川,阴山下。
    天似穹庐,笼盖四野。
    天苍苍,野茫茫,风吹草低见牛羊。

    这首诗歌的具体是作者和时间并没有特别明确的记载,有一种说法是该歌为“斛律金”所作,但更有可能是他曾经在关键时刻带领将士吟唱过。据记载,在高欢在玉璧之战大败后,斛律金为高欢部高唱此歌,最终稳住军心。

    所以,这是应该是一首鲜卑语的歌曲,怀念鲜卑人所生活的“刺勒川”美景。

    这首词经常被选入小学一二年级的课文。一方面,这首词语言精炼易懂,描述的景色开阔壮美、自然恬静;另一方面,这首歌为少数来自少数名族的词,也有表达中华民族是一个多民族融合的大家族。

    7.3 花木兰

      唧唧复唧唧,木兰当户织。不闻机杼声,唯闻女叹息。

      问女何所思,问女何所忆。女亦无所思,女亦无所忆。昨夜见军帖,可汗大点兵,军书十二卷,卷卷有爷名。阿爷无大儿,木兰无长兄,愿为市鞍马,从此替爷征。

      东市买骏马,西市买鞍鞯,南市买辔头,北市买长鞭。旦辞爷娘去,暮宿黄河边,不闻爷娘唤女声,但闻黄河流水鸣溅溅。旦辞黄河去,暮至黑山头,不闻爷娘唤女声,但闻燕山胡骑鸣啾啾。

      万里赴戎机,关山度若飞。朔气传金柝,寒光照铁衣。将军百战死,壮士十年归。

      归来见天子,天子坐明堂。策勋十二转,赏赐百千强。可汗问所欲,木兰不用尚书郎,愿驰千里足,送儿还故乡。

      爷娘闻女来,出郭相扶将;阿姊闻妹来,当户理红妆;小弟闻姊来,磨刀霍霍向猪羊。开我东阁门,坐我西阁床。脱我战时袍,著我旧时裳。当窗理云鬓,对镜帖花黄。出门看火伴,火伴皆惊忙:同行十二年,不知木兰是女郎。

      雄兔脚扑朔,雌兔眼迷离;双兔傍地走,安能辨我是雄雌?

    “木兰辞”被认为是一首创作于南北朝时期,但经过后世多次修改后最终形成我们现在看到的样子。在北朝(北魏/东西魏/北周/北齐),多使用“世兵制”或“府兵制”,“军户”在战时总是需要承担兵役,即便出现“阿爷无大儿,木兰无长兄”的情况也不能豁免。于是就有了木兰,以女儿身代父从军的故事。

    7.4 桃花源记

    … 土地平旷,屋舍俨然。有良田、美池、桑竹之属。阡陌交通,鸡犬相闻 … 自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋…

    陶渊明 《桃花源记》

    这首诗歌作于东晋、刘宋时期,在那前后的百年间,中国一直处于战乱之中。作者虚构此故事,表达了对和平安宁的向往。

    另外,我们加两个娃的名字,即来自于这首诗的“阡陌交通,鸡犬相闻”。一方面,希望他两兄弟能够拥有诗词中的生活。再者,一阡一陌,各有自己的方向与人生,但也有相交之处,这个相交的地方大概就是我们这个家吧。

    7.5 乌衣巷

    朱雀桥边野草花,乌衣巷口夕阳斜
    旧时王谢堂前燕,飞入寻常百姓家

    刘禹锡 《乌衣巷》

    这也是一首非常有名的怀古诗,作者站在乌衣巷前,看着“寻常”景色,遥想当年权倾一世的“王”、“谢”,早已随着时间而消失于历史长河。

  • 大语言模型的一个重要方向是“推理”优化,即如何在有限的硬件环境中提升推理的效率。对于所有的 MaaS 服务提供方,这都是至关重要的。一方面关乎用户的使用体验(诸如TTFT,time to first token)、另一方面关于服务提供的成本(有限的GPU如何提供更高的吞吐量)。

    1. 概述

    从 Transformer 架构的 Decoder 阶段原理来看,一个常见的、自然的优化就是使用“KV Cache”大大减少推理(自回归阶段)过程需要计算量,实现以显存换效率,从而加速推理过程。

    2. Decoder 模型的自回归计算

    在了解了“Attention”、“mask attention”、“autoregression”计算之后,比较自然可以注意到在 Q、K、V 矩阵在“autoregression”的过程中,有很多的部分是无需额外计算的。

    这里依旧继续使用《理解大语言模型的核心:Attention》中的示例,这里考虑在文章中的提示词“It’s very hot in summer. Swimming is”,生成新的Token为 “ a”,那么我们看看这个自回归过程某个Head中的计算。完成的代码可以参考:autoregression-of-attention.ipynb

    相比与在 prefill 阶段,需要额外计算的,在后续使用黄色标识出来。

    2. 1 Token Embedding 和 Positional Embedding

    Token Embedding

    +

    Positional Embedding

    这里,只需要计算最新的Token(即这里的“ a”)的Embedding即可。事实上,上面矩阵白色部分再自回归阶段完全不再需要使用了。所以,上述内容计算完成后,内存即可释放,无需缓存。

    2. 2 Normalize

    即,将每一个token的embedding 进行正规化,将其均值变为0,方差变为1

    与前面类似,这里计算完成并推进到下一步后,内存即可释放,无需缓存。

    2. 3 Attention 层的参数矩阵

    \(W^Q\,,W^K\,,W^V \)

    2. 4 矩阵 Q K V的计算

    \(Q = XW^Q \)

    \(K = XW^K \)

    \(V = XW^V \)

    2. 5 计算 Attention Score

    \(\text{Attention Score} \)

    \(= \frac{QK^T}{\sqrt{d}} \)

    特别需要注意的,这一步中,“Attention Score Matrix”最后一行的计算,需要前面的Q的最后一行,此外还需要整个 K 矩阵。这就是为什么 K 矩阵是需要缓存的。

    2. 6 计算 Masked Attention Score

    \(\text{Masked Attention Score} \)

    \(= \frac{QK^T}{\sqrt{d}} + \text{mask} \)

    2.7 计算 Softmax Masked Attention Score

    \(\text{Softmax Masked Attention Score} \)

    \(= \text{softmax}(\frac{QK^T}{\sqrt{d}} + \text{mask}) \)

    2. 8 计算 Contextual Embeddings

    \(\text{Contextual Embeddings} \)

    \(= \text{softmax}(\frac{QK^T}{\sqrt{d}} + \text{mask})V \)

    所以,这一步中,“Contextual Embeddings”最后一行的计算,需要前面 Softmax Masked Attention Score Matrix 的最后一行,此外还需要整个 V 矩阵。这就是为什么 V 矩阵是需要缓存的。

    此外可以看到,在这个自回归的计算中,Q 矩阵前面的所有行(即上一轮计算的Q矩阵)都用不上,这也是为什么 Q 矩阵不需要缓存,即我们需要的“KV Cache”,而不是“QKV Cache”的原因。

    3. 计算图示

    这里依据使用了图示的方式展示了在“自回归”过程中的数学计算。在下图中,第一个生成的 Token 为“ a”,该 Token 在进入 Decoder 模型再次进行计算时(即“自回归”),下图中:

    • 粉红色背景部分为新的、需要计算的部分;
    • 灰色背景部分为虽然不需要计算,但在计算新的内容时,需要使用的部分。

    灰色部分即为“KV Cache”需要缓存的部分。即,每一个 Token 对应的 “K”、“V” 矩阵都需要在后续的计算中使用。亦即,每一个 Token 的 Key 向量都需要保存,用于与新的 Token 的 Query 向量进行点击计算“关注度”值;每一个 Token 的 V 向量也需要保持

    在上述的计算中,注意到,在一次的新的“自回归”中,最终需要额外计算的就是新Token(这里是“ a”)对应的 Centextual Embedding,该内容计算,需要使用前述所有 Token 对应的 K、V 值,即这里的 K 和 V 矩阵。

    所以,在一次自回归推理中,最好上一次计算的所有 Token 的 K、V 向量都缓存起来,避免重复计算。本次自回归中计算新Token的对应的 K、V 向量也需要缓存,以供后续使用。

    4. KV Cache 的内存消耗

    在推理优化中,一个重要硬限制便是GPU卡的显存(memory)大小。当前,主流的企业级显卡H100显存为80GB,高端显卡 H200 显存为141 GB。现在的 LLM 参数量通常巨大,参数加载就需要耗费巨大的显存,以最新的 llama 4 17B为例,考虑 FP16 (半精度)考虑,则需要消耗约 30+ GB 。卡片上剩余的内存,才是用于实际的推理使用。而每次推理,例如提示词是1000个Token,输出也是1000个Token,那么,在生成最后一个Token的时候,需要的内存(按5%的经验值计算)约为1.5GB。这时候,单个H100的显卡也只能支持约33个并发,实际的情况则要考虑系统内存等,会比这个预估多很多。

    在这篇文章:Mastering LLM Techniques: Inference Optimization@developer.nvidia.com 中也类似的估算:

    • 7B 的模型(如Llama 2 7B),参数是16位(FP16 or BF16)则参数需要消耗约 14 GB 显存
    • Token 数为4096的推理(decoder),则需要约 2 GB KV Cache

    从上述粗略的预估可以看得出来,高效使用显存资源对于 LLM 推理来说至关重要。所以,各推理框架则会通过各种方法尝试去优化“KV Cache”以降低显存使用。这些方法包括“量化”(Quantization)、MQA/MGA 等。

    5. Multi-Query Attention/Group-Query Attention

    可以看到,无论是在模型参数加载的时候,还是推理 KV Cache 阶段,都需要大量的显存。关于 MQA 和 GQA 的经典论文是:GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints

    5.1 关于MQA与GQA

    Multi-Query Attention 则尝试通过减少 \(W^K \, W^V \) 参数的数量来减少上述显存,从而增加推理速度与并发能力。参考下图,可以看到在每一个 Layer 中,所有的 Head 共享一组 \(W^K \, W^V \) 参数,那么这两个相关参数就减少到了原来的 \(\frac{1}{h} \)。

    更进一步的,为了减少上述方法(MQA)对于模型效果的影响,另一个优化是 Group-Query Attention。即如下图,一组 Heads 共享一组 \(W^K \, W^V \) 。可以依照分组的大小,以平衡模型效果与资源使用。如果一个 Head 一组 \(W^K \, W^V \) 则退化到普通的 Multi-Head Attention;如果所有 Heads 分到一组,则退化到普通的 Multi-Query Attention。

    5.2 模型训练 Uptraining

    此外,比较关键的,论文提出了一些关于 GQA 架构的训练优化。

    例如,从一个 MHA 架构开始训练,然后从某个 checkpoint 开始,将MHA模型改成GQA模型,在初始化分组参数时,则使用原 MHA 模型中参数去求一个均值的方式初始化GQA中对应的 \(W^K \, W^V \) 。然后继续使用语料库对于该新模型训练。

    论文指出,这时候只需要使用非常少的计算资源就可以训练处效果还不错的GQA新模型。新的GQA模型,则可以使用更少的显存资源,有更好的并发吞吐能力,同时也达到还比较好的效果。

    参考

  • 在整个大语言模型学习之路中,对 Attention 机制的理解大概是最为让我困惑的部分,最终经过层层解构、加上重新把“线性代数”温习了一遍之后,最终,总算某种程度的理解了 Attention 机制的设计。相信对于所有NLP专业的人,这部分都是不太容易理解的。

    1. 概述

    要想讲清楚,大概也是非常不容易的,这里就做一个尝试吧。这里的重点是讲清楚 Attention Score (简称Attention)的计算。介绍的顺序是“两个词语的相似度”、“Similarity Score Matrix”、“Attention Score Matrix”。

    1.1 要构建的是直觉,而不是“推理”

    为什么 Attention 理解起来很难呢?我想其中有一个原因大概是这个“机制”本身并不是某种“公式推导”出来的,而是通过一篇篇论文与实践,被证明非常有效的一个机制,所以,这个机制本身的所具备的“可解释性”其实也是有限的。这大概也是,无论你在互联网上如何搜索,也没有谁可以比较简单的把这个机制说清楚的原因。但,理解这个机制构建的直觉,对于理解整个 Transformer ,以及整个当代大语言模型技术基础都是至关重要的。

    2. 预处理

    在“大语言模型的输入:tokenize”中详细介绍了“提示词”进入大模型处理之前,如何将提示词换行成大模型可以处理的“词向量”或者说“token embedding”。

    大语言模型在开始“Attention”计算之前,还会对“token embedding”进行一些预处理,这些预处理包括了“融入”位置向量、对向量进行“归一化”处理(将各个向量都转化为均值为0、方差为1的向量,长度要统一变成1吗?)。

    例如,在这里的例子中,提示词 “It’s very hot in summer. Swimming is”,先转换为embedding,然后加上位置编码(positional encoder)、再进行正规化,最后变换为如下的向量 “ X ” :

    这里的 “X” 是一个由12个 “token embedding”组成的矩阵,“形状”是 12 x 768 。在数学符号上,有:

    3. Similarity Score Matrix

    在正式介绍 Attention 之前,为了能够比较好的理解“为什么”是这样,这里先引入了“Similarity”的概念,最终在该概念上,新增权重矩阵,就是最终的 Attention :

    $$ \text{Similarity} = \text{softmax}(\frac{XX^{T}}{\sqrt{d}})X $$

    这里删除了参数矩阵:\(W^Q \quad W^K \quad W^V \)

    $$ \text{Attention} = \text{softmax}(\frac{QK^{T}}{\sqrt{d}})V $$

    其中, \(Q = XW^Q \quad K = XW^K \quad V = XW^V \)

    3.1 两个“词语”的相似度

    在向量为单位长度的时候,通常可以直接使用“内积”作为两个向量的相似度度量。例如,考虑词语 “hot” 与 “summer” 的相似度,则可以“简化”的处理这两个词(Token)的向量的“内积”。

    在前面的文章“大语言模型的输入:tokenize”中,较为详细的介绍了大语言模型如何把一个句子转换成一个的 Token,然后再转换为一个个“向量”。那么,我们通常会通过两个向量的余弦相似度来描述其相似度,如果向量的“长度”(\(L_2 \) 范数)是单位长度,那么也通常会直接使用“内积”描述两个向量的相似度:

    $$
    \cos \theta = \frac{\alpha \cdot \beta}{\|\alpha\| \|\beta\| }
    $$

    \(f(x) = \cos(x) \) 的图像如右图,故:

    • 夹角为 0 时,最为相似,这时候 \(\cos(x) = 1 \)
    • 夹角 \(\pi \) 时,最“不”相似,这时候 \(\cos(x) = 0 \)

    例如,

    3.2 “Similarity Score Matrix”

    因为两个向量的“内积”某种程度可以表示为相似度。那么,对于句子中的某个 token 来说,与其他所有向量各自计算“内积”,就可以获得一个与其他所有向量“相似程度”的数组,再对这个数组进行 softmax 计算就可以获得一个该 token 与其他所有向量“相似程度”的归一化数组。这个归一化的数据,就可以理解为这里的“Similarity Score Matrix”。

    这里依旧以“大语言模型的输入:tokenize”示例中的句子为演示示例。

    更为具体的,可以参考右图。这里考虑 Token “It” 与其他所有词语的相似度。即计算 Token “It” 的 Embedding 向量,与其他所有向量的“内积”。

    更进一步,如果计算两两词语之间的相似度,并进行归一化(softmax ),则有如下的Similarity Matrix:

    在这个示例中,则会有上述 12×12 的矩阵。该矩阵反应了“词”与“词”之间的相似度。如果,我们把每一行再进行一个“归一化”(注右图已经经过了归一化),那么每一行,就反应了一个词语与其他所有词语相似程度的一个度量。

    例如,右图中 it 可能与 very 最为相似(除了自身)。

    4. Self-Attention

    4.1 对比

    注意到最终的 “Attention” 计算公式和上述的“Similarity Score Matrix”的差别就是参数矩阵W:

    $$ \text{Similarity Score} = \text{softmax}(\frac{XX^{T}}{\sqrt{d}})X $$

    这里没有参数矩阵:\(W^Q \quad W^K \quad W^V \)

    $$ \text{Attention} = \text{softmax}(\frac{QK^{T}}{\sqrt{d}})V $$

    其中, \(Q = XW^Q \quad K = XW^K \quad V = XW^V \)

    4.2 为什么需要参数矩阵 W

    那么,为什么需要 \(W^Q \,, W^K \,, W^V \) 呢?这三个参数矩阵乘法,意味着什么呢?要说清楚、要理解这个点并不容易,也没有什么简单的描述可以说清楚的,这也大概是为什么,对于非 NLP 专业的人,要想真正理解 Transformer 或 Attention 是比较困难的。

    你可能会看到过一种比较普遍的、简化版本,大概是说 \(W^Q \) 是一个 Query 矩阵,表示要查询什么;\(W^K \) 是一个 Key 矩阵,表示一个词有什么。这个说法似乎并不能增加对上述公式的理解。

    那么,一个向量乘以一个矩阵时,这个“矩阵”意味着什么?是的,就是“线性变换”。

    4.3 线性变换 Linear Transformations

    一般来说,\(W^Q \,, W^K \,, W^V \) 是一个 \(d \times d \) 的矩阵[1]。对于的 Token Embedding (上述的矩阵 X )所在的向量空间,那么 \(W^Q \,, W^K \,, W^V \) 就是该向量空间的三个“线性变换”。

    那么线性变换对于向量空间的作用是什么呢?这里我们以“奇异值分解”的角度来理解这个问题[2],即对向量进行拉伸/压缩、旋转、镜像变换。\(W^Q \,, W^K \,, W^V \) 则会分别对向量空间的向量(即Token Embedding)做类似的变换。变换的结果即为:

    $$ Q = XW^Q \quad \quad K = XW^K \quad \quad V = XW^V $$

    那么,如果参数矩阵“设计”合理,“Token”与“Token”之间就可以建立“期望”的 Attention 关系,例如:“代词”(it),总是更多的关注于“名词”;“名词”更多的关注与附近的“形容词”;再比如,“动词”更多关注前后的“名词”等。除了词性,线性变换关注的“维度”可能有很多,例如“位置”、“情感”、“动物”、“植物”、“积极/消极”等。关于如何理解 token embedding 的各个“维度”含义可以参考:Word Embedding 的可解释性探索

    当然,这三个参数矩阵都不是“设计”出来的,而是“训练”出来的。所以,要想寻找上述如此清晰的“可解释性”并不容易。2019年的论文《What Does BERT Look At? An Analysis of BERT’s Attention》较为系统的讨论了这个问题,感兴趣的可以去看看。

    关于线性变换如何作用在向量空间上,可以参考:线性代数、奇异值分解–深度学习的数学基础

    所以,\( \frac{QK^T}{\sqrt{d}} = \frac{XW^Q (XW^K)^T}{\sqrt{d}} \) 则可以系统的表示,每个“Token”对于其他“Token”的关注程度(即pay attention的程度)。可以注意到:

    • 增加了参数矩阵\(W^Q \,, W^K \,, W^V \)后,前面的“相似性”矩阵,就变为“注意力”矩阵
    • “Token” 之间的关注程度不是对称的。例如 Token A 可能很关注 B;但是 B 可能并不关注 A
    • 这里的 \(\sqrt{d} \) 根据论文描述,可以提升计算性能;

    如果,你恰好理解了上面所有的描述,大概会有点失望的。就只能到这儿吗?似乎就只能到这里了。如果,你有更深刻的理解,欢迎留言讨论。

    接下里,我们来看看 “Attention Score Matrix” 的计算。

    4.4 Attention Score Matrix

    使用上述的 “Similarity Score Matrix” 的计算方式,可以计算类似的 “Attention Score Matrix”,之后再对该矩阵进行 softmax 计算就可以获得每个词语对于其他所有词语的 Attention Score,或者叫“关注程度”。有了这个关注程度,再乘以 V 矩阵,原来的 Token Embedding 就变换为一个新的带有上下文的含义的 Token Eembedding 了,即 Context Embedding[3]

    类似的,我们有右图的 Attention Score Matrix 计算。

    该矩阵反应了两个 Token 之间的 Attention 关系。该关系,通过对经过线性变换的 Token Embedding ,再进行内积计算获得。

    4.5 Masked Attention Score Matrix

    上述计算,是一个典型的 Self Attention 计算过程,BERT 模型就使用类似的计算,但 GPT 模型(或者叫 Decoder 模型)还有一些不同。GPT 模型中为了训练出更好的从现有 Token 中生成新 Token 的模型,将上述的 Self Attention 更改成了 Masked Self Attention ,即将 Attention Score Matrix 的右上角部分全部置为 -inf (即负无穷),后续经过 softmax 之后这些值都会变成零,即,在该类模型下,一个词语对于其后面的词的关注度为 0 。

    在 Decoder 模型设计中,为了生成更准确的下一个 Token 所以在训练和推理中,仅会计算Token 对之前的 Token 的 Attention ,所以上述的矩阵的右上角部分就会被遮盖,即就是右侧的 “Masked Attention Score Matrix”。

    通常为了快速计算对于上述的计算值会除以 \(\sqrt{d} \) ,可以提升计算的效率。

    4.6 归一化(softmax) Attention Score

    对于上述矩阵的每一行都进行一个 softmax 计算,就可以获得一个归一化的按照百分比分配的Attention Score。

    经过归一化之后,每个词语对于其他词语的 Attention 程度都可以使用百分比表达处理。例如,“summer”对于“It”的关注程度最高,为26%;其次是关注“hot”,为15%。可以看到这一组线性变换(\(W^Q\,W^K \))对于第一个位置表达特别的关注。

    4.7 Contextual Embeddings

    最后,再按照上述的 Attention Matrix 的比例,将各个 Token Embedding 进行一个“加权平均计算”。

    例如,上述的加权计算时,“summer” 则会融入 26% 的“It”,15%的“hot”… ,最后生成新的 “summer” 的表达,这个表达也可以某种程度理解为 “Contextual Embeddings”。需要注意的是,这里在计算加权平均,也不是直接使用原始的 Token Embedding ,也是一个经过了线性变换的Embedding,该线性变换矩阵也是经过训练而来的,即矩阵 \(W^V \)。

    例如,上述的加权计算时,“summer” 则会融入 26% 的“It”,15%的“hot”… ,最后生成新的 “summer” 的表达,这个表达也可以某种程度理解为 “Contextual Embeddings”。

    需要注意的是,这里在计算加权平均,也不是直接使用原始的 Token Embedding ,也是一个经过了线性变换的Embedding,该线性变换矩阵也是经过训练而来的,即矩阵 \(W^V \)。

    5 计算示意图

    如下的示意图,一定的可视化的表达了,一个 Token 如何经过上述的矩阵运算,如何了其他 Token 的内容。

    6. 注意力矩阵的观察

    那么,我们给定于如下的提示词输入:“Martin’s one of my sons, and the other is Chanler.”。看看在 GPT 模型中,各个Token之间的 Attention 情况:

    • 这句话总计有21个token,所以这是一个21×21的矩阵
    • 这里是“masked self-attention”,所以矩阵的右上半区都是 “0” 。
    • 在GPT2中,一共12层,每层12个“头”,所以一共有“144”个类似的矩阵
    • \(W^Q \,, W^K \,, W^V \) 的维度都是768×64,所以粗略的估计这部分的参数量就超过2000万,具体的:

    768*64*3*144 = 21,233,664

    7. Multi-Head Attention

    7.1 Scaled Dot-Product Attention

    以前面小结“预处理”中的 X 为例,Attention Score Matrix 就有如下的计算公式:

    $$
    \begin{aligned}
    \text{Attention Score Matrix} & = \text{softmax}(\frac{QK^T}{\sqrt{d}}) \\
    & = \text{softmax}(\frac{XW^Q(XW^K)^T}{\sqrt{d}})
    \end{aligned}
    $$

    最终的 Attention 计算如下:

    $$
    \begin{aligned}
    \text{Attention}(Q,K,V) & = \text{softmax}(\frac{QK^T}{\sqrt{d}})V \\
    & = \text{softmax}(\frac{XW^Q(XW^K)^T}{\sqrt{d}})XW^V
    \end{aligned}
    $$

    7.2 Multi-Head Attention

    上述的“Attention”在论文“Attention Is All You Need”称为“Scaled Dot-Product Attention”。更进一步的在论文中提出了“Multi-Head Attention”(经常被缩写为“MHA”)。对应的公式如下(来自原始论文):

    $$
    \begin{aligned}
    \text{MultiHead}(Q, K, V) & = \text{Concat}(\text{head}_1, \dots, \text{head}_h)W^O \\
    \text{where} \quad \text{head}_i & = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
    \end{aligned}
    $$

    更完整的解释,可以参考原始论文。这里依旧以前文的示例来说明什么是MHA。

    在本文第4章“Self-Attention”中,较为详细的介绍了相关的计算(即模型的推理过程)。在示例中,一共有12个“Token”,在进入 Attention 计算时经过位置编码、正则化后,12个“Token”向量组成矩阵“X”,这里的“X”的 shape 为 12 x 768,通常使用符号 \(l \times d \) 或者 \(l \times d_{model} \) 表示。最终输出的 Contextual Embedding 也是 \(l \times d_{model} \) 的一组表示12个 Token 向量,这是每个向量相比最初的输入向量,则融合上下文中其他词语的含义。在一个多层的模型中,这组向量则可以作为下一层的输入。

    在“Multi-Head Attention”其输入、输出与“Self-Attention”一样,都是 \(l \times d_{model} \) 。但是,对于最终输出的 \(l \times d_{model} \) 的向量/矩阵,在 MHA 中则分为多个 HEAD 各自计算其中的一部分,例如,一共有 \(d_{model} \) 列,那么则分别有 \(h \) 个HEAD,每个 HEAD 输出其中的 \(\frac{d_{model}}{h} \) 列。在上述示例中,供有12个HEAD,即 \(h=12 \),模型维度为768,即\(d_{model} = 768 \),所以每个HEAD,最终输出 \(64 = \frac{d_model}{h} = \frac{768}{12} \) 列。即:

    $$
    \begin{aligned}
    \text{where} \quad \text{head}_i & = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
    \end{aligned}
    $$

    然后由12个 Head 共同组成(concat)要输出的 Contextual Embedding,并对此输出做了一个线性变换\(W^O \)。即:

    $$
    \begin{aligned}
    \text{MultiHead}(Q, K, V) & = \text{Concat}(\text{head}_1, \dots, \text{head}_h)W^O
    \end{aligned}
    $$

    7.3 Self Attention vs MHA

    Self Attention


    Multi Head Attention


    7.4 Multi-Head Attention 小结

    • “Multi-Head Attention” 与 经典“Attention” 有着类似的效果,但是有着更好的表现性能
    • “Multi-Head Attention” 与 经典“Attention” 有相同的输入,相同的输出

    8. Attention 数学计算示意图

    如下的图片,半可视化的展示了在GTP2中,某一个HEAD中Attention的计算。

    9. 全流程数学计算

    完整的计算,就是一个“forward propagation”或者叫“inference”的过程,这里依旧以上述的提示词“It’s very hot in summer. Swimming is”,并观察该提示词在 GPT2 模型中的第一个Layer、第一个Head中的计算。完成的代码可以参考:Attention-Please.ipynb

    9.1 Token Embedding 和 Positional Embedding

    Token Embedding

    +

    Positional Embedding

    9.2 Normalize

    即,将每一个token的embedding 进行正规化,将其均值变为0,方差变为1

    9.3 Attention 层的参数矩阵

    \(W^Q\,,W^K\,,W^V \)

    9.4 矩阵 Q K V的计算

    \(Q = XW^Q \)

    \(K = XW^K \)

    \(V = XW^V \)

    9.5 计算 Attention Score

    \(\text{Attention Score} \)

    \(= \frac{QK^T}{\sqrt{d}} \)

    9.6 计算 Masked Attention Score

    \(\text{Masked Attention Score} \)

    \(= \frac{QK^T}{\sqrt{d}} + \text{mask} \)

    9.7 计算 Softmax Masked Attention Score

    \(\text{Softmax Masked Attention Score} \)

    \(= \text{softmax}(\frac{QK^T}{\sqrt{d}} + \text{mask}) \)

    9.8 计算 Contextual Embeddings

    \(\text{Contextual Embeddings} \)

    \(= \text{softmax}(\frac{QK^T}{\sqrt{d}} + \text{mask})V \)

    10. 其他

    上述流程详述了 LLM 模型中 Attention 计算的核心部分。也有一些细节是省略了的,例如,

    • 在GPT2中,“线性变换”是有一个“截距”(Bias)的,所以也可以称为一个“仿射变换”,即在一个线性变换基础上,再进行一次平移;
    • 在GTP2中,Attention计算都是多层、多头的,本文主要以Layer 0 / Head 0 为例进行介绍;
    • 在生成最终的“Contextual Embeddings”之前,通常还需要一个MLP层(全连接的前馈神经网络)等,本文为了连贯性,忽略了该部分。

    总结一下,本文需要的前置知识包括:矩阵基本运算、矩阵与线性变换、SVD 分解/特征值特征向量、神经网络基础、深度学习基础等。

    • [1] 多头注意情况可能是 \(d \times \frac{d}{h} \)
    • [2] 对于满秩方阵也可以使用“特征值/特征向量”的方式去理解
    • [3] 在 GPT 2 中,一个 Attention 层的计算,会分为“多头”去计算;并在计算后,还会再经过一个 MLP 层
  • 通史阅读:隋与唐

    ·

    起初读历史总还是带着一些功利心的,总是想着这些知识能有些什么用处。读着读着,发现了解这些历史本身就是有趣的、愉快的,虽然有时候也觉得很沉重,完全不用有什么功利心。读着读着,也有一些所思、所想,所以也就记录下来,存放于此。

    1. 底层英雄与数代积累

    隋、唐两个王朝家族都与“北魏”(包括北齐、北周)有着非常大的关系。杨坚之父杨忠,为西魏/北周建国“十二将军”将军之一;李渊祖父李虎为“西魏八大柱国”之一,都是当时极其显赫的家族。

    北朝统治者都是鲜卑人,而当时在中国北部的汉人“胡化”也非常深,杨坚、李渊家族也都有各自的鲜卑姓氏,分别是“普六茹”和“大野氏”。

    有少部分是底层“英雄”振臂一呼“王侯将相宁有种乎”后揭竿而起,但更多的王朝迭代都是数代人的积累后,再加上天时地利与人和,隋、唐的建立就是典型代表。隋文帝其父杨忠已经是北周重臣,其女则是北周宣帝宇文赟的皇后,其妻为西魏“八柱国”之一独孤信之女独孤伽罗,“禅让”皇位给杨坚的北周静帝即为宇文赟之子(注:并不杨坚之女所生)。

    而后续的唐替隋也有一些类似。唐高祖李渊的祖父李虎,与当时的宇文泰、独孤信、李弼等同为西魏八柱国,后李虎追随宇文泰一起建立北周,对抗北齐高欢集团。而当时的隋炀帝杨广则是李渊的姨表亲(注:李渊父取独孤信四女;杨坚取独孤信七女)。

    历史上,真正完全从底层如爽文般一路打拼成就帝业的是非常少,比较有名的大概就是汉高祖、明太祖、北齐高欢、后赵石勒等。大多数王朝或割据政权的建立,则都是历经数代人的累计。

    2. 王朝的建立之初的平乱

    一般王朝多建立于乱世,而在王朝建立之初,多半都需要平定各地分散的势力,才能最终建立统一的、稳固的王朝统治,隋初、唐初都有类似的局面。杨坚在拥立北周静帝时,平定了相州、郧州、益州等地叛乱;而后经“禅让”自立,先是北御突厥,待北方略微安定,再南征陈朝,最后与589年灭陈。至此,才真正建立的大一统的“隋”。

    而唐初局面也非常混乱。在李渊最初控制了长安之后,全国局势依旧非常混乱。北边突厥异常强大,洛阳则有王世充、河北有窦建德、另外还有实力强大的瓦岗军(翟让/李密/秦琼/程咬金等)等。最终,直到唐武德七年,全国才全部统一。而在完全统一后的两年,就发生了改变历史进程的“玄武门之变”。

    3. 总出问题的“国本”

    帝王的继承问题,历来都是难以解决的“困局”。隋、唐两朝在初代都出现了比较严重的国本之“战”。隋文帝起初虽然立长子杨勇,但后来受多方影响,废杨勇,而改立杨广。而杨广很快矫召赐死杨勇(“矫称高祖之诏,赐故太子勇死,缢杀之” 资治通鉴/卷180)。

    而唐初的“玄武门之变”则震惊历代看客,期间李世民射杀兄弟、逼迫其父退位。之后,太宗一朝虽然政治清明,制度逐渐完善,社会相对进入一段安定时期,并且太宗作为君主确实有非常多的了不起的地方(尤其是在宽宏用人这个方面),但,“玄武门之变”对于人伦观念的摧残,对后世影响大概是巨大的。在“后武则天”时代,李隆基诛杀安乐公主(堂妹),而后又诛杀太平公主(姑姑),最终掌权,开创“开元盛世”。帝王之家的人伦、亲情,大概不能用常理去理解吧。

    4. 关于历史真实性的问题

    理科生读历史大概总是比较较真,总会问出“这些事情都是真的吗?”。诸如鸿门宴的时候,并无外人在场,但“史记”却写得如此生动。所以,这些历史都是真的吗?

    4.1 附会

    关于这个问题并没有一个简单的回答。因为无论回答是什么,都没有人能够像“理科”一样去证明、或者去通过重复试验验证。但,学者们对此是有共识的。在读吕思勉的《中国通史》、《三国史话》时,印象最深的一词大概就是“附会”。例如:

    《三国志》上所说的兵谋,大都是靠不住的。这大约因军机秘密,局外人不得而知,事后揣测,多系附会之谈,而做历史的人所听见的,也不过是这一类的话之故。

    吕思勉 《三国史话 袁绍和曹操的战争》

    历史上的重大事件,多半都是真实发生过的,因为大事件经常会有多方记载,可以相互印证。再者,对于大的事件,因为知晓的人太多,即便是当权者也通常难以抹除。但,“篡改”、“重新构建”、丑化前朝、美化当朝则是经常有的,甚至是某种常态。

    4.2 贞观君臣的史实构建

    至于,大事件中的各种细节,则需要甄别,可能经过一些有意无意的修改或者“重构”,这是非常常见的。在看此类内容时,则通常需要考虑多方印证、编写者的时代、编写者的立场、甚至性格等综合因素去做一些“猜测”或“揣摩”。

    例如,“玄武门之变”这场政变本身的记录是无法修改,但是关于这个事件的“叙事”史实的记录中大概是“真正假假”的,需要非常专业的甄别。关于这一点可以参考西北大学历史学院院长李军的一次在线分享《玄武门之变的真相与贞观君臣的史实建构》。从这个示例中,可以看到史官如何系统的、“精细”的“修改”历史,以满足当朝者的意志。

    4.3 李渊的“久畜叛志”

    … 突厥来侵,李渊遣军拒之,未能取胜,深恐获罪。于是世民等乘机劝渊举事,经过屡次的催促,他才决心采取行动。这是李渊起兵最通行的说法,但从若干史事的迹象看来,李渊本人可能是这次起兵的发动着,他富于才略,久畜叛志,世民只是为其效力建功而已[ 参看唐温大雅《大唐创业起居注》,罗香林《大唐创业起居注》考证(载罗著《唐代文化史研究》),李树桐《李唐太原起义考实》(载《大陆杂志》六卷十期及十一期) ]…

    傅乐成 《中国通史》 p335/336

    傅乐成认为在后续的历史资料中,为了强调李世民“夺位”的“合法性”,有意夸大了李世民在起兵中的作用。而这种“修改”、“构建”在历史记录中大概是非常常见。

    5. 胡汉血统

    中国非常大,南北差异也非常大,民族融合问题在很多朝代都有着各种挑战,即便是在当代,也时有类似问题出现。

    但从历史角度来看,这些问题有时候是比较奇怪的。现在中国人,有时候会被称作“唐人”,而海外更是很多的国家都以“唐人街”命名中国人聚集的地方。现在的中国人,也一般都以唐朝的强盛而自豪。但是,简单的从“血统”来说,李世民可能有非常多的鲜卑族的“血统”。至少,其母亲太穆皇后窦氏为鲜卑人、其祖母独孤氏也是鲜卑人。如果再算到唐高宗李治,因为其母是长孙皇后也是鲜卑人,其血统的鲜卑成分可能就更高了。

    历史上,唐太宗李世民除了称皇帝外,也称“天可汗”,即其不仅是中原之首,也是周边一众其他民族之首。

    所以,“胡汉”自很早以前,其界限就不再清晰了。更不用说,自北魏孝文帝以来,胡汉已经很大程度在进行融合,鲜卑人很可能也融合了很多的汉人的血统。如果要简单的从家族去论“胡汉”早就已经不太成立了。如果真的要较真的话,则大概只能去看“生物”(DNA)层面。

    6. 武周与李唐

    在看这段历史时,最错愕的大概是,武则天在处理“杀伐废立”时的果断,竟如历代“君王”一般,在消灭异己、拉拢人心、政事执行上,也显得娴熟老练。后续大概还想再细读一下武则天的一生,到底是怎样的早年经历或者成长环境,催生出如此人性如此冷酷、果决的一面。

    起初,被唐高宗皇后王氏引入,以对抗萧淑妃。在武则天获宠之后,很快就借机以“谋行鸩毒”废王皇后与萧淑妃(“谋行鸩毒,废为庶人,母及兄弟,并除名,流岭南”),并最终杀害之。在废王皇后之后,武则天也很快被册立为皇后(临轩命司空李x继玺绶册皇后武氏)。而当初曾经反对立其为后的重臣,如“长孙无忌韩瑗来济褚遂良”都被流放、贬官或赐死。而后来,高宗有一些厌恶武后的专权(“初,武后得志,遂牵制帝,专威福,帝不能堪”),有意废后的时候,找到宰相上官仪商量,上官仪表示支持并说:“皇后专恣,海内失望,宜废之以顺人心。”(新唐书-上官列传)。上官仪后来也被杀,其子上官庭芝亦被杀,其孙女婉儿被“配掖廷”(新唐书-后妃列传-上官昭容)。在上官婉儿被重用后,上官仪才被追赠为“中书令、秦州都督、楚国公”。

    高宗死后,中宗即位的,数月后,武则天即废中宗,改立李旦(即唐睿宗),自己则临朝称制,自专朝政。很快,武太后(此时为太后)就遇到了第一个“大”的考验–徐敬业等的反叛(当时骆宾王为此写了一篇千古檄文《为徐敬业讨武曌檄》),此叛虽然很快就平定了。但是,武太后大概意识到,在自己真正掌权的路上还会有很多的“挑战”。在接下来几年,重用了一众“酷吏”,又大开告密之门,从而借此大肆剪除异己。

    很快,又发生了琅邪王李冲(太宗之孙)的叛乱、越王李贞(太宗之子、李冲之父)呼应。但两人都没有组织起真正的力量,叛乱很快被平。这次叛乱牵连的李氏诸王很多,包括迫“韩王李元嘉(高祖子)、鲁王李灵夔(高祖子)、黄国公李譔(高祖孙、李元嘉之子)、东莞郡公李茂融(高祖孙)、常乐公主(高祖之女)等”,多被赐死或自缢。

    之后,很快武皇后便正式废睿宗而称帝(690年),在位15年,于神龙元年(705年)政变,禅位于中宗,武则天这时八十一岁,并于同年崩逝。

    7. 唐玄宗、杨贵妃与安史之乱

    在中国千百年的帝王制度之下,皇帝本身“质量”的优劣很大程度上决定了国家兴衰,而皇帝“质量”和普通人其实是一样的,时好时坏,更多的时候是平庸;也有的人可能年轻时很励精图治,年迈后则多贪图权势与享乐,这其实都是人之常情。

    唐玄宗彻底结束了李唐王朝中“女强人”干政带来的不稳定因素,并且在其在位的中前期,能够任用贤臣,使得唐朝社会再次在安定中发展。虽然这里提到的“彻底结束”过程也是艰难的,先是诛“韦后”、“安乐公主”,后又灭“太平公主”,最后才真正掌权。玄宗在位44年,也因为其在位前期、后期完全不同的“历史形象”,所以经常会看到人感叹:如果玄宗少活几年,大概也是一位在历史上能够排名前几的明君了。

    哎,“太平公主”是玄宗亲姑姑、“韦后”是他“婶婶”、“安乐公主”是他堂姐妹,不得不再次感叹自古帝王家的人伦之情与百姓之家是完全不同的。大概真的如韩非所言:“情非憎君也,利在君之死也”,可怕!

    玄宗晚年宠信杨贵妃“一家”,任用李林甫、杨国忠,最终导致“安史之乱”。期间,李唐皇室之狼狈,前所未有!安禄山破潼关后入长安时,玄宗仓皇出逃,到马嵬驿经历短暂的兵变,杨贵妃被赐死。而后,太子李亨与玄宗分道,北上收拾残兵以抵御“安史”之乱。太子很快就自行在灵武宣布即帝位,是为唐肃宗,遥尊玄宗为太上皇。最后,因郭子仪等众将征伐,以及“安史”自身的内乱,最终叛乱被平。

    乱虽然平了,但是自此地方藩镇势力割据、中央控制大大削弱,加上皇室内部宦官持续当权,也导致唐朝最后的一百多年多处藩镇“自治”之中(注:虽然在宪宗时期虽短暂终结藩镇割据)。

    到唐末,先是小范围的“民变/兵变”,最终导致“大范围”民变(王仙芝与黄巢)与藩镇自立。此时的唐室中央政权早已羸弱不堪,最终,叛乱看似平定,但是留下的两大军事势力“沙陀李克用部”、“黄巢旧部降将朱温部”,以及一众不再受控制的地方藩镇势力。

    唐昭宗时期宰相崔胤,为灭宦官,召朱全忠(朱温)入关。宦官确实被成功消灭了,但是,也引狼入室,最终导致自己被杀,唐室也最终被朱温所灭。朱温建国为梁,中国也进入了地方自治的“五代十国时期”。

    8. 再读唐诗

    了解了这些,打算再读读自己以前背过的一些唐诗。《早发白帝城》、《长恨歌》、《春望》、《石壕吏》。

    杜甫 《石壕吏》

    暮投石壕村,有吏夜捉人。
    老翁逾墙走,老妇出门看。
    吏呼一何怒,妇啼一何苦。
    听妇前致词:三男邺城戍,
    一男附书至,二男新战死。
    存者且偷生,死者长已矣。
    室中更无人,惟有乳下孙。
    有孙母未去,出入无完裙。
    老妪力虽衰,请从吏夜归。
    急应河阳役,犹得备晨炊。
    夜久语声绝,如闻泣幽咽。
    天明登前途,独与老翁别。

    杜甫的一生大概是颠沛流离的。这首诗写于杜甫在安史之乱“流离”期间,是其著名的“三吏三别”之一,也曾入选初高中的教材之中。

    诗中,家中老翁为了躲避征军,暂且躲避,由老妪面对官吏。老妪的一家三个儿子都已经被征召,孙子尚小,家中已无人能够去服役,最后,老妪自己被征召,去给军队做饭。独留老翁一人,支撑支离破碎的家。

    再读 “吏呼一何怒,妇啼一何苦”,仿佛能够听到 1200 年前,征军人员的凶狠,以及百姓的无奈。

    杜甫 《春望》

    国破山河在,城春草木深。
    感时花溅泪,恨别鸟惊心。
    烽火连三月,家书抵万金。
    白头搔更短,浑欲不胜簪。

    这首诗写于安史之乱期间,当时叛军攻陷长安,玄宗幸蜀,杜甫“流离”陷于长安,写下此诗。

    “国破山河在,城春草木深” 写的是景,述的是感伤。而其中“国破山河在” 一句更是为古今中外所赞叹(Sangaria)。

    李白 《早发白帝城》

    朝辞白帝彩云间,千里江陵一日还。
    两岸猿声啼不住,轻舟已过万重山。

    安史之乱期间,永王(玄宗十六子)被认为谋反,而李白曾为永王幕僚而被牵连。后,李白并被流放夜郎。在押送到白帝城时,被收到被赦免的消息,故做《早发白帝城》。

    了解这个背景,再读“轻舟已过万重山”,可以感受到当时,李白突然一身轻松的感觉。

    9. 一些关键事件的事件图

    右图是关于安史之乱的主要内容,由 ChatGPT 生成。

    对于历史事件,我总是感觉难以记忆,所以打算探索一下使用图片辅助记忆的方式去增强理解与记忆。

    以前,我也会看到一些这样的制作精美的“图片”,但是看起来并不是很有感觉,现在发现,并不是这些图片制作的不好,而是没有加上自己的理解、没有记忆的动力,所以即便看到了一些很好的、帮助记忆的内容,也是难以记忆的。

  • 对于非 NLP 专业的人来说,要向理解大语言模型的基础其实是非常不容易的。在有了一定的神经网络基础、数学基础之后,算是可以更进一步了,在了解LLM的系列中,大概可以分成几个部分:输入(即本文的tokenize)、计算(Attention)、输出(beam search/top-k)。在本篇中:(a) 通过代码实践观察大模型(GPT2)如何进行 tokenize;(b) 如何查看 token id 列表 (c) 观察模型中所有token的 Embedding Matrix。

    这是我的大语言(LLM)学习系列中的一篇,完整的学习路径参考:我的大模型学习路线

    1. 理解 tokenize

    1.1 Token ID

    这里我们使用如下的提示词,来看看大模型是如何处理的:It’s very hot in summer. Swimming is

    大模型会使用预先设计好的“tokenize”实现,将上述的句子分解成独立的“Token”,并转换为对应的“Token ID”,而每个Token,都有自己的编码,也就是 Embedding ,这些 Embedding 就最终作为大语言模型的输入。

    对于上述的句子,“openai-community/gpt2” 在进行 tokenize 之后对应的 Token 和 Token ID 如下:

    itsveryhotinsummer . Swimmingis
    Token ID1026447247 82 84530242873931 13 245127428318
    TokenItâĢĻsĠveryĠhotĠinĠsummer.ĠSwimmingĠis

    “openai-community/gpt2” 使用了较为常见的BPE(Byte Pair Encoding)对句子进行处理,把每个词语按照“subword”进行处理,例如:

    • “Swimming”拆分为“Sw”与“imming”
    • 这里两个 Token 447、247 组成特殊字符 ‘ (撇号)
    • Ġ(U+0120)的作用:表示这是一个新的词语(而不是一个拆分后子词),可以看到 “Swimming”在拆分后的“imming”前面并没有Ġ,表示这是一个拆分后的子词

    1.2 词表大小

    “openai-community/gpt2” 模型的词表大小为:50257。词表中的前三个 Token 为 ’emb’、 ‘ĠDraft’、 ‘Ġreinvent’,对应的 Token ID 为 24419、 13650、 36608。

    1.3 根据 Token ID 打印字符

    这里打印了 Token ID 为 0、 1、 2 和 50254、 50255、 50256 的几个字符如下:

    Token IDChar
    0!
    1
    2#
    50254Ġinformants
    50255Ġgazed
    50256<|endoftext|>
    --- Token ID 转换为 Token 字符 ---
    Token ID: 0 | 对应 Token 字符: '!'
    Token ID: 1 | 对应 Token 字符: '"'
    Token ID: 2 | 对应 Token 字符: '#'
    Token ID: 50254 | 对应 Token 字符: 'Ġinformants'
    Token ID: 50255 | 对应 Token 字符: 'Ġgazed'
    Token ID: 50256 | 对应 Token 字符: '<|endoftext|>'

    2. Token Embedding

    在大模型的通常是从 Embedding 开始的,即对于所有字符的处理,都是依赖字符对应的“向量”。所以,大致的处理逻辑是这样:一个句子,先切分为 Token,然后根据 Token ID 在“Embedding Matrix”中找到对应的“向量”,把该“向量”组作为输入。

    这里,我们观察上述示例句子,根据对应的 Token ID 可以查询到对应的向量。因为这里的向量是768维的,这里仅显示前三个维度的分量,如下:

    Token      | wte[:3]                         |
    ----------------------------------------------
    It | [0.039, -0.0869, 0.0662 ,...] |
    âĢ | [-0.075, 0.0948, -0.0034,...] |
    Ļ | [-0.0223, 0.0182, 0.2631 ,...] |
    s | [-0.064, -0.0469, 0.2061 ,...] |
    Ġvery | [-0.0553, -0.0348, 0.0606 ,...] |
    Ġhot | [0.0399, -0.0053, 0.0742 ,...] |
    Ġin | [-0.0337, 0.0108, 0.0293 ,...] |
    Ġsummer | [0.0422, 0.0138, -0.0213,...] |
    . | [0.0466, -0.0113, 0.0283 ,...] |
    ĠSw | [0.0617, 0.0373, 0.1018 ,...] |
    imming | [-0.1385, -0.1774, -0.0181,...] |
    Ġis | [-0.0097, 0.0101, 0.0556 ,...] |

    更为完整的,上述的每个词语对应的向量是一个 1×768 的向量;整个句子 12 个向量,可以理解为一个 12×768 的输入矩阵。对于各 LLM 来说,这通常就是其将要处理的输入。

    3. Positional encoding

    在“GPT-2”使用了“Learned Positional Embeddings”(注:与 Transformer 论文中固定的Sinusoidal 实现不同)。这是一个 \(L \times d \) 的矩阵,其中,\(L \) 是最大接收字符数,\(d \) 是 Token Embedding 的维度。该矩阵通常随机初始化,最终通过训练确定。在“GPT-2”中,最终训练后的矩阵有如下形式:

    pos_emb_layer = model.transformer.wpe
    
    # .weight.data : Embedding Matrix(PyTorch Tensor)
    pos_emb_matrix = pos_emb_layer.weight.data
    
    print(pos_emb_matrix[:12,:3])

    输入如下:

    tensor([[-0.0188, -0.1974,  0.0040],
            [ 0.0240, -0.0538, -0.0949],
            [ 0.0042, -0.0848,  0.0545],
            [-0.0003, -0.0738,  0.1055],
            [ 0.0076, -0.0251,  0.1270],
            [ 0.0096, -0.0339,  0.1312],
            [ 0.0027, -0.0205,  0.1196],
            [ 0.0025, -0.0032,  0.1174],
            [-0.0012, -0.0018,  0.1110],
            [ 0.0049,  0.0021,  0.1178],
            [ 0.0016,  0.0062,  0.1004],
            [-0.0036,  0.0175,  0.1068]])

    示例与代码

    环境准备

    !pip install transformers torch
    
    from transformers import AutoTokenizer, AutoModelForCausalLM
    
    MODEL_NAME = "openai-community/gpt2"
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)

    对句子进行 tokenize

    观察句子的tokenize:

    text = "It’s very hot in summer. Swimming is"
    
    # 1. 对句子进行 Tokenization
    # return_tensors='pt' 表示返回 PyTorch Tensor 格式 (虽然这里我们主要看 IDs)
    inputs = tokenizer(text, return_tensors='pt')
    
    # 2. 打印 Tokenization 结果
    print(f"--- 原始句子:{text} ---")
    
    # a. 打印 Token ID Tensor
    print("Token IDs (Tensor):")
    print(inputs['input_ids'])
    
    # b. 将 Token ID 转换回可读的 Token (Word Pieces)
    # .squeeze() 是为了去除 batch 维度
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'].squeeze().tolist())
    print("\nToken List (可读文本 Tokens):")
    print(tokens)
    
    # c. 打印 Attention Mask (1 表示是有效 Token,0 表示是 Padding Token)
    print("\nAttention Mask:")
    print(inputs['attention_mask'])
    
    --------------
    --- output ---
    --------------
    
    --- 原始句子:It’s very hot in summer. Swimming is ---
    Token IDs (Tensor):
    tensor([[ 1026,   447,   247,    82,   845,  3024,   287,  3931,    13,  2451,
             27428,   318]])
    
    Token List (可读文本 Tokens):
    ['It', 'âĢ', 'Ļ', 's', 'Ġvery', 'Ġhot', 'Ġin', 'Ġsummer', '.', 'ĠSw', 'imming', 'Ġis']
    
    Attention Mask:
    tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])

    词表大小查看

    查看词表大小,并打印词表中的前20个词:

    # 获取词汇表大小
    vocab_size = tokenizer.vocab_size
    print(f"--- GPT-2 模型支持的 Tokenize 总数 (词汇表大小): {vocab_size} ---")
    
    # 打印所有 Tokenize
    # tokenizer.get_vocab() 返回的是一个字典 {token: id}
    print("\n--- 打印前 20 个 Token (用于示例): ---")
    vocab = tokenizer.get_vocab()
    count = 0
    for token, id in vocab.items():
        if count < 20:
            # 使用 repr() 确保特殊字符(如空格 'Ġ')能被清晰展示
            print(f"ID: {id:<5} | Token: {repr(token)}")
            count += 1
        else:
            break
    
    --------------
    --- output ---
    --------------
    
    --- GPT-2 模型支持的 Tokenize 总数 (词汇表大小): 50257 ---
    
    --- 打印前 20 个 Token (用于示例): ---
    ID: 24419 | Token: 'emb'
    ID: 13650 | Token: 'ĠDraft'
    ID: 36608 | Token: 'Ġreinvent'
    ID: 36171 | Token: 'Recommended'
    ID: 20706 | Token: 'aunting'
    ID: 39558 | Token: 'Ġprotagonists'
    ID: 49309 | Token: 'raised'
    ID: 20589 | Token: 'Ġwicked'
    ID: 43074 | Token: 'ĠâĿ'
    ID: 22792 | Token: 'ĠTut'
    ID: 21620 | Token: 'erate'
    ...

    根据 Token ID 打印字符

    # 待查询的 Token ID 列表
    target_ids = [0, 1, 2, 50254 ,50255, 50256]
    
    tokens = tokenizer.convert_ids_to_tokens(target_ids)
    
    print(f"--- Token ID 转换为 Token 字符 ---")
    for id, token in zip(target_ids, tokens):
        # 使用 repr() 确保任何特殊的不可见字符(如空格或控制字符)能被清晰地展示
        print(f"Token ID: {id:<5} | 对应 Token 字符: {repr(token)}")
    
    # 额外打印这几个 Token 在词汇表中的 ID,以确认其对应关系
    # 这里的 token.id 并不是直接从 1, 2, 3 来的,而是从 tokenizer.get_vocab() 中查到的
    # 这是一个辅助验证步骤,确保 Tokenizer 的行为符合预期。
    print("\n--- 辅助验证 ---")
    for token in tokens:
        token_id_check = tokenizer.convert_tokens_to_ids(token)
        print(f"Token 字符: {repr(token):<10} | 查验 ID: {token_id_check}")
    
    --------------
    --- output ---
    --------------
    
    --- Token ID 转换为 Token 字符 ---
    Token ID: 0     | 对应 Token 字符: '!'
    Token ID: 1     | 对应 Token 字符: '"'
    Token ID: 2     | 对应 Token 字符: '#'
    Token ID: 50254 | 对应 Token 字符: 'Ġinformants'
    Token ID: 50255 | 对应 Token 字符: 'Ġgazed'
    Token ID: 50256 | 对应 Token 字符: '<|endoftext|>'

    根据 Token ID 查询对应向量

    在模型中,“词向量”(准确的应该是Token向量)存储在一个Embedding Matrix 中,可以使用如下的代码获取每个 token 对应 Embedding 后的向量:

    target_token = "ĠSw"  # 注意前面的特殊符号,确保它是模型词汇表中的 Token
    target_id = tokenizer.convert_tokens_to_ids(target_token)
    print(target_id)
    
    # embedding_matrix[target_id]
    target_embedding = embedding_matrix[target_id]
    print(target_embedding[:5].numpy())
    
    --------------
    --- output ---
    --------------
    
    2451
    [ 0.06167513  0.03733223  0.10182938  0.04881619 -0.09597597]
    Embedding 层的 dtype: torch.float32
    整个模型的默认 dtype: torch.float32

    句子到 Embedding 向量

    text = "It’s very hot in summer. Swimming is"
    inputs = tokenizer(text, return_tensors='pt')
    print("Token IDs (Tensor):")
    input_ids_tensor = inputs['input_ids']
    input_ids_list = input_ids_tensor.squeeze().tolist()
    
    for index, token_int in enumerate(input_ids_list):
        token_char = tokenizer.convert_ids_to_tokens(token_int)
        print(f"Token ID {token_int:<5} | Token: {repr(token_char):<9} | {embedding_matrix[token_int][:3]}")
    
    --------------
    --- output ---
    --------------
    
    Token ID 1026  | Token: 'It'      | tensor([ 0.0390, -0.0869,  0.0662])
    Token ID 447   | Token: 'âĢ'      | tensor([-0.0750,  0.0948, -0.0034])
    Token ID 247   | Token: 'Ļ'       | tensor([-0.0223,  0.0182,  0.2631])
    Token ID 82    | Token: 's'       | tensor([-0.0640, -0.0469,  0.2061])
    Token ID 845   | Token: 'Ġvery'   | tensor([-0.0553, -0.0348,  0.0606])
    Token ID 3024  | Token: 'Ġhot'    | tensor([ 0.0399, -0.0053,  0.0742])
    Token ID 287   | Token: 'Ġin'     | tensor([-0.0337,  0.0108,  0.0293])
    Token ID 3931  | Token: 'Ġsummer' | tensor([ 0.0422,  0.0138, -0.0213])
    Token ID 13    | Token: '.'       | tensor([ 0.0466, -0.0113,  0.0283])
    Token ID 2451  | Token: 'ĠSw'     | tensor([0.0617, 0.0373, 0.1018])
    Token ID 27428 | Token: 'imming'  | tensor([-0.1385, -0.1774, -0.0181])
    Token ID 318   | Token: 'Ġis'     | tensor([-0.0097,  0.0101,  0.0556])
    

    Embedding Matrix

    查看 Embedding Matrix

    # model.transformer.wte (Word Token Embeddings)
    embedding_layer = model.transformer.wte
    
    # .weight.data : Embedding Matrix(PyTorch Tensor)
    embedding_matrix = embedding_layer.weight.data
    
    print(embedding_matrix.shape)
    
    --------------
    --- output ---
    --------------
    
    torch.Size([50257, 768])

    最后

    大模型训练的第一步就是对于语料库(corpus)的处理,即将所有的语料转换为大模型训练能够接受的输入,即:tokenize。该过程会将语料库切分为独立的句子,多个句子可以作为一个批次(batch)作为输入进行训练。

  • 整体上,今年的魔力象限与去年的厂商完全相同,各厂商的相对位置变化也并不是很大。一些值得注意的点如下:

    • Redis 从 Visionaries 跌到 Niche Player 象限;这反应了 Redis 在社区所面临的困境,一方面是开源商业化的挑战;另一方,则是来自于 Valkey 社区–一个更加开放的 Key-Value 产品的竞争。
    • Neo4j 也从 Visionaries 跌到 Niche Player 象限。
    • 在第一军团(即Google AWS Microsoft Oracle)中,Oracle 位置略有下降。确实,Oracle 早就已经不再是一家数据库厂商了。
    • Databricks 和 Snowflake 凭借在数据处理上的领先,在横坐标(Visionaries)上前进了一大截
    • 此外,虽然没有在象限图中(仅前20的厂商),但依旧在Gartner关注对象中的厂商包括:Actian Broadcom ClickHouse InfluxData MotherDuck OceanBase PingCAP Tencent Cloud TigerGraph Yugabyte

    象限中的中国数据库厂商

    进入这次魔力象限的中国厂商与去年相同:阿里云数据库、华为云数据库。相比去年,两个厂商的位置变化也不太大,可以参考右图。

    阿里云数据库在 Vision 象限继续向前移动了一点。华为云则保持了相对位置几乎不变。

    此外,出现在“Honorable Mentions”部分的中国厂商有:

    • OceanBase
    • PingCAP
    • Tencent Cloud

    历史魔力象限列表

    2025-11

    其他

    作者最近几年持续对 Gartner 云数据库魔力象限保持关注,历史相关文章包括: