DIYgod / RSSHub

🧡 Everything is RSSible
https://docs.rsshub.app
MIT License
33.49k stars 7.5k forks source link

知乎用户动态activities路由失效 #11882

Closed Liu-Guang closed 1 year ago

Liu-Guang commented 1 year ago

路由地址

/zhihu/people/activities/:id

完整路由地址

/zhihu/people/activities/diygod

相关文档

https://docs.rsshub.app/social-media.html#zhi-hu-yong-hu-dong-tai

预期是什么?

正常抓取知乎用户动态

实际发生了什么?

Response code 403 (Forbidden): target website might be blocking our access

部署

RSSHub 演示 (https://rsshub.app)

部署相关信息

Ubuntu 20.04.5 LTS

额外信息

尝试更换了几次服务器的ip,依然是403 Forbidden

这不是重复的 issue

github-actions[bot] commented 1 year ago
Searching for maintainers:

To maintainers: if you are not willing to be disturbed, list your username in scripts/workflow/test-issue/call-maintainer.js. In this way, your username will be wrapped in an inline code block when tagged so you will not be notified.

如果有任何路由无法匹配,issue 将会被自动关闭。如果 issue 和路由无关,请使用 NOROUTE 关键词,或者留下评论。我们会重新审核。 If there is any route not found, the issue will be closed automatically. Please use NOROUTE for a route-irrelevant issue or leave a comment if it is a mistake.

bigfei commented 1 year ago

standalone rsshub instance also get blocked with

Route requested: /people/activities/bopengbopeng
Error message: Response code 403 (Forbidden): target website might be blocking our access, you can [host your own RSSHub instance](https://docs.rsshub.app/install/) for a better usability.
Helpful Information to provide when opening issue:
Path: /people/activities/bopengbopeng
Node version: v16.19.0
Git Hash: ae3bccd
YukiSou commented 1 year ago

happened also on self host instance

Error message: Error: Failed to extract `d_c0` from cookies
    at /root/RSSHub/lib/v2/zhihu/activities.js:27:19
    at runMicrotasks ()
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.tryGet (/root/RSSHub/lib/middleware/cache/index.js:64:21)
    at async module.exports (/root/RSSHub/lib/v2/zhihu/activities.js:15:24)
    at async /root/RSSHub/node_modules/koa-mount/index.js:58:5
    at async /root/RSSHub/node_modules/koa-mount/index.js:52:26
    at async /root/RSSHub/lib/middleware/load-on-demand.js:29:9
    at async /root/RSSHub/lib/middleware/cache/index.js:122:13
    at async /root/RSSHub/node_modules/koa-mount/index.js:52:26

Helpful Information to provide when opening issue:
Path: /people/activities/diygod
Node version: v16.15.1
Git Hash: fbccaec
TonyRL commented 1 year ago

/test

/zhihu/people/activities/diygod
github-actions[bot] commented 1 year ago

Successfully generated as following:

http://localhost:1200/zhihu/people/activities/diygod - Failed ``` HTTPError: Response code 404 (Not Found) Route requested: /people/activities/diygod Error message: Response code 403 (Forbidden): target website might be blocking our access, you can host your own RSSHub instance for a better usability. Helpful Information to provide when opening issue: Path: /people/activities/diygod Node version: v18.14.0 Git Hash: fbccaec ```
KwToPA commented 1 year ago

https://github.com/DIYgod/RSSHub/issues/11868

自建和官方都被封IP

KwToPA commented 1 year ago

这个问题应该是一周或者两周前出现的

确切日期记不清了

UniSpaceX commented 1 year ago

在国内VPS上运行 docker image id 74d8a76af600,提示报错:

Route requested: /people/activities/nogirlnotalk Error message: Failed to extract d_c0 from cookies Helpful Information to provide when opening issue: Path: /people/activities/nogirlnotalk Node version: v16.19.0 Git Hash: fbccaec

宽带公网IP 手动部署最新master,第一次运行时 RSS拉取成功,后续报错:

Route requested: /people/activities/nogirlnotalk Error message: Response code 403 (Forbidden): target website might be blocking our access, you can host your own RSSHub instance for a better usability. Helpful Information to provide when opening issue: Path: /people/activities/nogirlnotalk Node version: v18.12.1 Git Hash: unknown

浏览器登录知乎官网提示:

系统监测到您的网络环境存在异常,为保证您的正常访问,请点击下方验证按钮进行验证。在您验证完成前,该提示将多次出现

手动验证通过之后,又可以成功进行一至两次 RSS拉取,后续重复报错。 应该是知乎的反爬限制提高了。

abolast commented 1 year ago

在国内VPS上运行 docker image id 74d8a76af600,提示报错:

Route requested: /people/activities/nogirlnotalk Error message: Failed to extract d_c0 from cookies Helpful Information to provide when opening issue: Path: /people/activities/nogirlnotalk Node version: v16.19.0 Git Hash: fbccaec

宽带公网IP 手动部署最新master,第一次运行时 RSS拉取成功,后续报错:

Route requested: /people/activities/nogirlnotalk Error message: Response code 403 (Forbidden): target website might be blocking our access, you can host your own RSSHub instance for a better usability. Helpful Information to provide when opening issue: Path: /people/activities/nogirlnotalk Node version: v18.12.1 Git Hash: unknown

浏览器登录知乎官网提示:

系统监测到您的网络环境存在异常,为保证您的正常访问,请点击下方验证按钮进行验证。在您验证完成前,该提示将多次出现

手动验证通过之后,又可以成功进行一至两次 RSS拉取,后续重复报错。 应该是知乎的反爬限制提高了。

看了你的反馈,我换了服务器ip,然后把请求频率从55min改成9h/次

KwToPA commented 1 year ago

@abolast 我停了两天再抓 依然封号 不过不是全封

全部N条,其中偶尔若干条解封,会抓一部分 之后又封,感觉这个解封是随机的

我抓取频率没改,15分钟一次

abolast commented 1 year ago

@abolast 我停了两天再抓 依然封号 不过不是全封

全部N条,其中偶尔若干条解封,会抓一部分 之后又封,感觉这个解封是随机的

我抓取频率没改,15分钟一次

知乎不干人事。我刚才又换了一个ip,抓一次然后马上失效。是不是知乎已经识别rsshub了。

KwToPA commented 1 year ago

@abolast 现在我关注的所有知乎路由都封了

Spartanmans commented 1 year ago

@abolast 现在我关注的所有知乎路由都封了

我也是,大概一周前

Reborn404 commented 1 year ago

知乎的都失效了

woodgear commented 1 year ago

+1

zhaojames0707 commented 1 year ago

+1

KwToPA commented 1 year ago

https://github.com/DIYgod/RSSHub/issues/11984

rsshub的tg群里有用户反馈 知乎路由复活了

我自建的机器报错从 Response code 403 (Forbidden): target website might be blocking our access

变成 Error: Failed to extract d_c0 from cookies

ICEY1W32 commented 1 year ago

同样的问题,自建被知乎反爬了

JamYiz commented 1 year ago

动态和专栏都失效了

jy324 commented 1 year ago

大佬们,会不会是因为知乎API从v3升级到了v4,所以按之前的爬很快就失效了? https://github.com/DIYgod/RSSHub/blob/b1400b97f41b4e442fc26cf1dafd78feb8b00e6c/lib/v2/zhihu/activities.js#L34 image

KwToPA commented 1 year ago

https://github.com/DIYgod/RSSHub/issues/11984#issuecomment-1474692455

我这边恢复了

KwToPA commented 1 year ago

我这边 一个半月没抓 每条知乎用户更新的内容只有7个

jy324 commented 1 year ago

你订阅了多少用户🤔

KwToPA commented 1 year ago

@jy324 我吗? 2K多

jy324 commented 1 year ago

不是😂rss里订阅了多少知乎用户

jy324 commented 1 year ago

我订阅了20个,怕到时量大给封了

KwToPA commented 1 year ago

800-1000这个级别

KwToPA commented 1 year ago

又变回 报错 Failed to extract d_c0 from cookies

/RSSHub/lib/v2/zhihu/activities.js:27:19

JamYiz commented 1 year ago

又变回 报错 Failed to extract d_c0 from cookies

/RSSHub/lib/v2/zhihu/activities.js:27:19

是不是你爬取的太多了?我昨天恢复之后就没有出错了

KwToPA commented 1 year ago

@JamYiz 现在的状态又变成 最初的 时而能抓几个,时而全报错 最新的报错

Response code 403 (Forbidden)

JamYiz commented 1 year ago

@JamYiz 现在的状态又变成 最初的 时而能抓几个,时而全报错 最新的报错

Response code 403 (Forbidden)

我只抓了个位数的源,没有出错了。大概率你是抓太多被办了。

jy324 commented 1 year ago

我也正常

TonyRL commented 1 year ago

/test

/zhihu/people/activities/diygod
github-actions[bot] commented 1 year ago

Successfully generated as following:

http://localhost:1200/zhihu/people/activities/diygod - Success ✔️ ```rss <![CDATA[DIYgod的知乎动态]]> https://www.zhihu.com/people/diygod/activities RSSHub i@diygod.me (DIYgod) zh-cn https://picx.zhimg.com/d8e0a6cc4f2ceb76fce450e6ca5f269c_l.jpg?source=5a24d060 <![CDATA[DIYgod的知乎动态]]> https://www.zhihu.com/people/diygod/activities Mon, 17 Jul 2023 14:44:36 GMT 5 <![CDATA[DIYgod关注了问题: 如何评价《龙之家族》(House of the dragon)第一季第十集?]]> Mon, 24 Oct 2022 17:07:31 GMT https://www.zhihu.com/question/561360428 https://www.zhihu.com/question/561360428 <![CDATA[DIYgod赞同了回答: 如何评价《龙之家族》(House of the Dragon)第十集?]]> 舅舅开泥头车吓唬骑自行车的外甥,因为雨天路滑把外甥撞死了

]]>
Mon, 24 Oct 2022 17:05:40 GMT https://www.zhihu.com/question/561357944/answer/2728955996 https://www.zhihu.com/question/561357944/answer/2728955996
<![CDATA[DIYgod赞同了回答: 戴安娜都离婚了为什么不影响威廉和哈里的继承权?]]> 天主教根本不承认“离婚”,

因为结婚誓言是在教堂里发下的,不可逆,

中世纪天主教所谓离婚

是由神职人员提供证据 证明夫妻二人婚姻无效,

最常用的借口就是近亲结婚无效,

古代欧洲贵族世代联姻,一找一个准。

婚姻是否无效 要看宗教领袖的意思,

当时的教皇 死活不肯给亨八走无效的手续,

导致亨八脱离天主教 自己单干。

戴安娜的年代 法律早就改了,允许正常离婚,

她的离婚是真离婚,

既然有婚可以离,他们之前的婚姻就是有效的,

而他们的宗教领袖就是女王本人,手续上不会有问题。

至于继承权,现在是议会说了算,按照议会颁布的一系列法律来确认,

法律承认的继承人,女王和教会都无法夺走其继承权。

]]>
Wed, 21 Sep 2022 23:01:24 GMT https://www.zhihu.com/question/552647417/answer/2681830181 https://www.zhihu.com/question/552647417/answer/2681830181
<![CDATA[DIYgod赞同了回答: 如何看待华为 CFO 孟晚舟称「华为规模变小了,但盈利能力增强了」?]]> 大公主仅仅只是说了“盈利能力增强了”,但华为的净利润究竟为何会出现增长?

大公主却只字未提。

根据华为公布的年报显示,2021年,华为的净利润为1137亿元,营业利润率(19.1%)则比2020年的营业利润率(8.1%)实现了翻倍。

然鹅,在华为的年报中,还有一项并不起眼的附件:“其他净收支”。其中,Top 2的两项净收入分别为:处置子公司及业务的净收益(574.31亿元)、政府补助(25.71亿元)。仅此两项,在华为2021年净利润中的比重就高达52.8%。

尤其是值得指出的是,处置子公司及业务的净收益(574.31亿元)主要来自出售荣耀手机。那么,这些钱都是由谁来买单的呢?

2020年11月,华为正式宣布出售荣耀。当时,为了避免荣耀手机破产而导致深圳供应商的破产连锁反应,深圳市智信新信息技术有限公司与华为签署了收购协议,由“智信新”作为收购方,完成对华为荣耀品牌相关业务资产的全面收购[1]

实际上,在华为出售荣耀2个月之前(2020年9月27日)才紧急成立的智信新,其股东仅包括2家机构:深圳国资委全资控股的深圳市智慧城市科技发展集团持股98.6%、深圳国资协同发展私募基金合伙企业[2]持股1.4%。

显然,与其说是“深圳国资携30余家供应商联手接盘”,还不如说是深圳国资委在用纳税人的钱为濒临破产[3]的荣耀买单。

到了2021年,同样的剧情又在华为X86服务器部门上演了一遍。而这一次接盘的,则是河南国资委。

也就是说,大公主口中所说的“盈利能力增强了”,实际上大部分都是来自于纳税人的钱。

如果考虑到手机业务熄火之后的华为主要依靠国内运营商的5G政治任务来“输血”(保命)的因素,那么,这个“盈利能力增强了”的money来源,就更加充满了铽奢煮意。

另一方面,“大跃进”式、大规模上马的5G政治任务,不仅没有实现国内网络的实质性提升[4],反而出现了下降。甚至,还出现了为了给5G“拉客”而强制对4G网络进行降速、甚至裁撤4G基站的骚操作。

实际上,从2019年开始,国内的4G网络就出现了大范围、肉眼可见的降速和信号等一系列问题[5]

对此,华为运营商BG总裁丁耘的解释则是,“只要5G用户渗透率足够高,一定会在商业回报上有所体现。”

嗯,这句话翻译过来就是:

]]>
Tue, 29 Mar 2022 16:23:10 GMT https://www.zhihu.com/question/524622426/answer/2413169696 https://www.zhihu.com/question/524622426/answer/2413169696
<![CDATA[DIYgod关注了问题: 上海东方医院回应「网传有护士哮喘发作去世」,有哪些信息值得关注?]]> 上海东方医院官网3月25日发布情况说明,全文如下:

周盛妮为我院护士,一直在院内从事科室相关工作。3月23日在家中哮喘发作,用药后无法缓解。19时许,家属驾车送其就诊。我院南院急诊部因疫情防控需要,正暂时关闭,进行环境采样和消毒,家属遂将病人送到仁济医院东院救治。23时许,周盛妮同志因抢救无效去世。

周盛妮同志工作勤恳,任劳任怨,是一名优秀的白衣天使。周盛妮同志的不幸去世,是我院的损失,我们对她的去世深感痛心,对她的亲属致以诚挚慰问!

来源:界面新闻

]]>
Sat, 26 Mar 2022 23:09:18 GMT https://www.zhihu.com/question/523983268 https://www.zhihu.com/question/523983268
<![CDATA[DIYgod关注了问题: 「我来 wolai」是可以用了一辈子不用换的笔记软件吗?]]> 最近在找适合自己的笔记工具,看到了我来,想来问问怎么样

]]>
Mon, 21 Feb 2022 19:31:38 GMT https://www.zhihu.com/question/500054607 https://www.zhihu.com/question/500054607
<![CDATA[DIYgod关注了问题: 《进击的巨人》末期的艾伦耶格尔的人设真的崩塌了吗?]]> 能不能用原作来解释说明他的行为到底是否合理?(合理不是指合乎我们的价值观 而是是否符合他在前作中的表现)

希望不会有纯玩梗的答案

我真的只想听听他到底为什么人设崩塌了

]]>
Sat, 05 Feb 2022 21:17:29 GMT https://www.zhihu.com/question/456031750 https://www.zhihu.com/question/456031750
```
TonyRL commented 1 year ago

/test

/zhihu/people/activities/diygod
/zhihu/people/activities/bopengbopeng
github-actions[bot] commented 1 year ago

Successfully generated as following:

http://localhost:1200/zhihu/people/activities/diygod - Success ✔️ ```rss <![CDATA[DIYgod的知乎动态]]> https://www.zhihu.com/people/diygod/activities RSSHub i@diygod.me (DIYgod) zh-cn https://picx.zhimg.com/d8e0a6cc4f2ceb76fce450e6ca5f269c_l.jpg?source=5a24d060 <![CDATA[DIYgod的知乎动态]]> https://www.zhihu.com/people/diygod/activities Thu, 07 Sep 2023 13:32:47 GMT 5 <![CDATA[DIYgod关注了问题: 如何评价《龙之家族》(House of the dragon)第一季第十集?]]> Mon, 24 Oct 2022 17:07:31 GMT https://www.zhihu.com/question/561360428 https://www.zhihu.com/question/561360428 <![CDATA[DIYgod赞同了回答: 如何评价《龙之家族》(House of the Dragon)第十集?]]> 舅舅开泥头车吓唬骑自行车的外甥,因为雨天路滑把外甥撞死了

]]>
Mon, 24 Oct 2022 17:05:40 GMT https://www.zhihu.com/question/561357944/answer/2728955996 https://www.zhihu.com/question/561357944/answer/2728955996
<![CDATA[DIYgod赞同了回答: 戴安娜都离婚了为什么不影响威廉和哈里的继承权?]]> 天主教根本不承认“离婚”,

因为结婚誓言是在教堂里发下的,不可逆,

中世纪天主教所谓离婚

是由神职人员提供证据 证明夫妻二人婚姻无效,

最常用的借口就是近亲结婚无效,

古代欧洲贵族世代联姻,一找一个准。

婚姻是否无效 要看宗教领袖的意思,

当时的教皇 死活不肯给亨八走无效的手续,

导致亨八脱离天主教 自己单干。

戴安娜的年代 法律早就改了,允许正常离婚,

她的离婚是真离婚,

既然有婚可以离,他们之前的婚姻就是有效的,

而他们的宗教领袖就是女王本人,手续上不会有问题。

至于继承权,现在是议会说了算,按照议会颁布的一系列法律来确认,

法律承认的继承人,女王和教会都无法夺走其继承权。

]]>
Wed, 21 Sep 2022 23:01:24 GMT https://www.zhihu.com/question/552647417/answer/2681830181 https://www.zhihu.com/question/552647417/answer/2681830181
<![CDATA[DIYgod赞同了回答: 如何看待华为 CFO 孟晚舟称「华为规模变小了,但盈利能力增强了」?]]> 大公主仅仅只是说了“盈利能力增强了”,但华为的净利润究竟为何会出现增长?

大公主却只字未提。

根据华为公布的年报显示,2021年,华为的净利润为1137亿元,营业利润率(19.1%)则比2020年的营业利润率(8.1%)实现了翻倍。

然鹅,在华为的年报中,还有一项并不起眼的附件:“其他净收支”。其中,Top 2的两项净收入分别为:处置子公司及业务的净收益(574.31亿元)、政府补助(25.71亿元)。仅此两项,在华为2021年净利润中的比重就高达52.8%。

尤其是值得指出的是,处置子公司及业务的净收益(574.31亿元)主要来自出售荣耀手机。那么,这些钱都是由谁来买单的呢?

2020年11月,华为正式宣布出售荣耀。当时,为了避免荣耀手机破产而导致深圳供应商的破产连锁反应,深圳市智信新信息技术有限公司与华为签署了收购协议,由“智信新”作为收购方,完成对华为荣耀品牌相关业务资产的全面收购[1]

实际上,在华为出售荣耀2个月之前(2020年9月27日)才紧急成立的智信新,其股东仅包括2家机构:深圳国资委全资控股的深圳市智慧城市科技发展集团持股98.6%、深圳国资协同发展私募基金合伙企业[2]持股1.4%。

显然,与其说是“深圳国资携30余家供应商联手接盘”,还不如说是深圳国资委在用纳税人的钱为濒临破产[3]的荣耀买单。

到了2021年,同样的剧情又在华为X86服务器部门上演了一遍。而这一次接盘的,则是河南国资委。

也就是说,大公主口中所说的“盈利能力增强了”,实际上大部分都是来自于纳税人的钱。

如果考虑到手机业务熄火之后的华为主要依靠国内运营商的5G政治任务来“输血”(保命)的因素,那么,这个“盈利能力增强了”的money来源,就更加充满了铽奢煮意。

另一方面,“大跃进”式、大规模上马的5G政治任务,不仅没有实现国内网络的实质性提升[4],反而出现了下降。甚至,还出现了为了给5G“拉客”而强制对4G网络进行降速、甚至裁撤4G基站的骚操作。

实际上,从2019年开始,国内的4G网络就出现了大范围、肉眼可见的降速和信号等一系列问题[5]

对此,华为运营商BG总裁丁耘的解释则是,“只要5G用户渗透率足够高,一定会在商业回报上有所体现。”

嗯,这句话翻译过来就是:

]]>
Tue, 29 Mar 2022 16:23:10 GMT https://www.zhihu.com/question/524622426/answer/2413169696 https://www.zhihu.com/question/524622426/answer/2413169696
<![CDATA[DIYgod关注了问题: 上海东方医院回应「网传有护士哮喘发作去世」,有哪些信息值得关注?]]> 上海东方医院官网3月25日发布情况说明,全文如下:

周盛妮为我院护士,一直在院内从事科室相关工作。3月23日在家中哮喘发作,用药后无法缓解。19时许,家属驾车送其就诊。我院南院急诊部因疫情防控需要,正暂时关闭,进行环境采样和消毒,家属遂将病人送到仁济医院东院救治。23时许,周盛妮同志因抢救无效去世。

周盛妮同志工作勤恳,任劳任怨,是一名优秀的白衣天使。周盛妮同志的不幸去世,是我院的损失,我们对她的去世深感痛心,对她的亲属致以诚挚慰问!

来源:界面新闻

]]>
Sat, 26 Mar 2022 23:09:18 GMT https://www.zhihu.com/question/523983268 https://www.zhihu.com/question/523983268
<![CDATA[DIYgod关注了问题: 「我来 wolai」是可以用了一辈子不用换的笔记软件吗?]]> 最近在找适合自己的笔记工具,看到了我来,想来问问怎么样

]]>
Mon, 21 Feb 2022 19:31:38 GMT https://www.zhihu.com/question/500054607 https://www.zhihu.com/question/500054607
<![CDATA[DIYgod关注了问题: 《进击的巨人》末期的艾伦耶格尔的人设真的崩塌了吗?]]> 能不能用原作来解释说明他的行为到底是否合理?(合理不是指合乎我们的价值观 而是是否符合他在前作中的表现)

希望不会有纯玩梗的答案

我真的只想听听他到底为什么人设崩塌了

]]>
Sat, 05 Feb 2022 21:17:29 GMT https://www.zhihu.com/question/456031750 https://www.zhihu.com/question/456031750
```

...

github-actions[bot] commented 1 year ago
http://localhost:1200/zhihu/people/activities/bopengbopeng - Success ✔️ ```rss <![CDATA[PENG Bo的知乎动态]]> https://www.zhihu.com/people/bopengbopeng/activities RSSHub i@diygod.me (DIYgod) zh-cn https://picx.zhimg.com/v2-c0e93926aa1716772a26677284a6bbec_l.jpg?source=5a24d060 <![CDATA[PENG Bo的知乎动态]]> https://www.zhihu.com/people/bopengbopeng/activities Thu, 07 Sep 2023 13:32:49 GMT 5 <![CDATA[PENG Bo赞同了文章: ChatRWKV 学习笔记和使用指南]]>

在这里插入图片描述

0x0. 前言

Receptance Weighted Key Value(RWKV)是pengbo提出的一个新的语言模型架构,它使用了线性的注意力机制,把Transformer的高效并行训练与RNN的高效推理相结合,使得模型在训练期间可以并行,并在推理的时候保持恒定的计算和内存复杂度。目前RWKV的社区已经非常火了,我们从huggingface上可以看到RWKV已经训练了多个百亿参数的模型,特别是RWKV World模型支持世界所有语言的生成+对话+任务+代码,功能十分全面。此外还有很多开发者基于RWKV的微调模型。


在部署方面RWKV社区也取得了长足的发展,例如ChatRWKV,rwkv.cpp,RWKV-Runner,rwkv-cpp-cuda,同时包括mlc-llm,tgi等都支持了RWKV相关模型,社区进展非常快。本文尝试从ChatRWKV项目入门学习一下RWKV。

RWKV论文原文:arxiv.org/abs/2305.1304

0x1. 学习资料

这里列出一些我看到的学习资料,方便感兴趣的读者学习:

原理推荐看第一个和第二个链接,其它的有选择观看,我这里就以ChatRWKV项目的解析为例来入门RWKV。

0x2. RWKV in 150 lines

下面这个文件 github.com/BlinkDL/Chat 以150行代码实现了RWKV-4-Pile-430M这个模型,是学习RWKV的最佳代码,所以让这一节就是来逐步解析一下这个代码。分析代码之前先对RWKV这个名字的含义和组成RWKV模型2个关键的元素Time Mixing和Channel Mixing简单描述一下,详细的原理还是请参考原始论文和第一节学习资料的第一个视频链接和第四个原理和公式详解的文字链接。

0x2.1 RWKV名字含义

论文中对名字有说明:

  • R: Receptance vector acting as the acceptance of past information. 类似于LSTM的“门控单元”
  • W: Weight is the positional weight decay vector. A trainable model parameter. 可学习的位置权重衰减向量,什么叫“位置权重衰减”看下面的公式(14)
  • K: Key is a vector analogous to K in traditional attention. 与传统自注意力机制
  • V : Value is a vector analogous to V in traditional attention. 与传统自注意力机制相同

0x2.2 RWKV模型架构

RWKV模型由一系列RWKV Block模块堆叠而成,RWKV Block的结构如下图所示:


RWKV Block又主要由Time Mixing和Channel Mixing组成。

Time Mixing模块的公式定义如下:


这里的t表示当前时刻,x_t看成当前的token,而x_{t-1}看成前一个token,r_tk_tv_t的计算与传统Attention机制类似,通过将当前输入token与前一时刻输入token做线性插值,体现了recurrence的特性。然后wkv_{t}的计算则是对应注意力机制的实现,这个实现也是一个过去时刻信息与当前时刻信息的线性插值,注意到这里是指数形式并且当前token和之前的所有token都有一个指数衰减求和的关系,也正是因为这样让wkv_{t}拥有了线性attention的特性。

然后RWKV模型里面除了使用Time Mixing建模这种Token间的关系之外,在Token内对应的隐藏层维度上RWKV也进行了建模,即通过Channel Mixing模块。

在这里插入图片描述

Channel Mixing的意思就是在特征维度上做融合。假设特征向量维度是d,那么每一个维度的元素都要接收其他维度的信息,来更新它自己。特征向量的每个维度就是一个“channel”(通道)。

下图展示了RWKV模型整体的结构:

在这里插入图片描述

这里提到的token shift就是上面对r, k, v计算的时候类似于卷积滑窗的过程。然后我们可以看到当前的token不仅仅剋呀通过Time Mixing的token shit和隐藏状态States(即wkv_{t})和之前的token建立联系,也可以通过Channel Mixing的token shift和之前的token建立联系,类似于拥有了全局感受野

0x2.3 RWKV_in_150_lines.py 解析

初始化部分

首先来看RWKV模型初始化部分以及最开始的一些准备工作:

########################################################################################################
# The RWKV Language Model - https://github.com/BlinkDL/RWKV-LM
########################################################################################################
# 导入库
import numpy as np
# 这行代码设置了numpy数组的打印格式,其中precision=4表示小数点后保留4位,
# suppress=True表示抑制小数点的科学计数法表示,linewidth=200表示每行的字符宽度为200。
np.set_printoptions(precision=4, suppress=True, linewidth=200)
import types, torch
from torch.nn import functional as F
from tokenizers import Tokenizer
# 加载一个分词器
tokenizer = Tokenizer.from_file("20B_tokenizer.json")
# 使用types.SimpleNamespace()创建一个简单的命名空间对象args,并为其设置以下属性:
args = types.SimpleNamespace()
# 模型的路径。
args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-pile-430m/RWKV-4-Pile-430M-20220808-8066'
args.n_layer = 24 # 模型的层数。
args.n_embd = 1024 # 模型的嵌入维度。
# 定义了需要续写的字符串,描述了科学家在西藏的一个偏远山谷中发现了一群会说流利中文的龙的情况。
context = "\nIn a shocking finding, scientist discovered a herd of dragons living in a remote, previously unexplored valley, in Tibet. Even more surprising to the researchers was the fact that the dragons spoke perfect Chinese."
NUM_TRIALS = 3 # 尝试生成文本的次数。
LENGTH_PER_TRIAL = 100 # 每次尝试生成的文本长度。
TEMPERATURE = 1.0 # 控制生成文本的随机性的参数。值越大,生成的文本越随机;值越小,生成的文本越确定。
TOP_P = 0.85 # 在生成文本时,只考虑累积概率超过此值的词汇。
########################################################################################################
class RWKV_RNN(torch.jit.ScriptModule):
    def __init__(self, args):
        super().__init__()
        # 将传入的args参数赋值给类的属性args。
        self.args = args
        # 将模型设置为评估模式,这意味着模型中的dropout和batchnorm将被禁用。
        self.eval() # set torch to inference mode
        # 从指定路径加载模型权重,并确保权重被加载到CPU上。
        w = torch.load(args.MODEL_NAME + '.pth', map_location='cpu')
        # 这几行代码对加载的权重进行了处理。它们检查权重的键名,并根据键名对权重进行不同的操作。
        for k in w.keys():
            if      '.time_' in k: w[k] = w[k].squeeze()
            if '.time_decay' in k: w[k] = -torch.exp(w[k].float()) # the real time decay is like e^{-e^x}
            else: w[k] = w[k].float() # convert to f32 type
        # 创建一个新的命名空间对象,并将其赋值给self.w。
        self.w = types.SimpleNamespace() # set self.w from w
        # 在self.w中创建一个名为blocks的字典。
        self.w.blocks = {}
        # for k in w.keys(): - 遍历字典w的所有键。注释中的例子
        # "blocks.0.att.time_first" => self.w.blocks[0].att.time_first"
        # 说明了代码的目标:将点分隔的键转换为嵌套的属性访问。
        for k in w.keys(): # example: "blocks.0.att.time_first" => self.w.blocks[0].att.time_first
            parts = k.split('.') #  使用.作为分隔符将键k分割成多个部分,并将结果存储在parts列表中。
            last = parts.pop() # 从parts列表中弹出最后一个元素并存储在last中。这将是要设置的属性的名称。
            #  初始化一个变量here,它将用于遍历或创建self.w中的嵌套命名空间。
            here = self.w
            # 遍历parts列表中的每个部分。
            for p in parts:
                # 检查当前部分p是否是数字。
                if p.isdigit():
                    p = int(p)
                    # 如果当前数字键p不在here中,则在here中为其创建一个新的命名空间。
                    if p not in here: here[p] = types.SimpleNamespace()
                    # 更新here以指向新创建的或已存在的命名空间。
                    here = here[p]
                # 如果当前部分p不是数字。
                else:
                    # 如果here没有名为p的属性,则为其创建一个新的命名空间。
                    if not hasattr(here, p): setattr(here, p, types.SimpleNamespace())
                    here = getattr(here, p)
            setattr(here, last, w[k])

这部分除了准备一些模型执行需要的超参数之外,还对RWKV模型进行了初始化,值得注意的是在初始化过程中会加载RWKV模型的权重到w这个字典里面,然后遍历字典w的所有键。注释中的例子"blocks.0.att.time_first" => self.w.blocks[0].att.time_first" 说明了代码的目标:将点分隔的键转换为嵌套的属性访问。后面推理的时候将会直接访问self.w这个处理之后的权重对象。

RWKV 模型通道融合函数(Channel mixing)

@torch.jit.script_method
    def channel_mixing(self, x, state, i:int, time_mix_k, time_mix_r, kw, vw, rw):
        xk = x * time_mix_k + state[5*i+0] * (1 - time_mix_k)
        xr = x * time_mix_r + state[5*i+0] * (1 - time_mix_r)
        state[5*i+0] = x
        r = torch.sigmoid(rw @ xr)
        k = torch.square(torch.relu(kw @ xk)) # square relu, primer paper
        return r * (vw @ k)

参考Channel Mixing的公式来看:

在这里插入图片描述

在channel_mixing函数里面,x_t对应当前token的词嵌入向量,state[5*i+0]表示前一个token的词嵌入向量。剩下的变量都是RWKV的可学习参数。然后代码里面会动态更新state,让state[5*i+0]总是当前token的前一个token的词嵌入。

RWKV Time mixing函数

@torch.jit.script_method
    def time_mixing(self, x, state, i:int, time_mix_k, time_mix_v, time_mix_r, time_first, time_decay, kw, vw, rw, ow):
        xk = x * time_mix_k + state[5*i+1] * (1 - time_mix_k)
        xv = x * time_mix_v + state[5*i+1] * (1 - time_mix_v)
        xr = x * time_mix_r + state[5*i+1] * (1 - time_mix_r)
        state[5*i+1] = x
        r = torch.sigmoid(rw @ xr)
        k = kw @ xk
        v = vw @ xv
        aa = state[5*i+2]
        bb = state[5*i+3]
        pp = state[5*i+4]
        ww = time_first + k
        qq = torch.maximum(pp, ww)
        e1 = torch.exp(pp - qq)
        e2 = torch.exp(ww - qq)
        a = e1 * aa + e2 * v
        b = e1 * bb + e2
        wkv = a / b
        ww = pp + time_decay
        qq = torch.maximum(ww, k)
        e1 = torch.exp(ww - qq)
        e2 = torch.exp(k - qq)
        state[5*i+2] = e1 * aa + e2 * v
        state[5*i+3] = e1 * bb + e2
        state[5*i+4] = qq
        return ow @ (r * wkv)

仍然是要对照公式来看:


然后这里有一个trick,就是对wkv_{t}的计算可以写成RNN的递归形式:

这样上面的公式就很清晰了,还需要注意的是在实现的时候由于有exp的存在,为了保证数值稳定性实现的时候减去了每个公式涉及到的e的指数部分的Max。

关于RWKV 的attention部分(wkv_{t})计算如果你有细节不清楚,建议观看一下这个视频:解密RWKV线性注意力的进化过程(bilibili.com/video/BV1z) 。

RWKV model forward函数

# 定义forward方法,它接受两个参数:token和state。
def forward(self, token, state):
        # 这是一个上下文管理器,确保在此代码块中不会计算任何梯度。
        # 这通常用于评估模式,以提高性能并避免不必要的计算。
        with torch.no_grad():
            # 如果state为None,则初始化state为一个全零张量。
            # 其形状由self.args.n_layer和self.args.n_embd确定。
            if state == None:
                state = torch.zeros(self.args.n_layer * 5, self.args.n_embd)
                # 遍历每一层,并将state的特定位置设置为-1e30(表示负无穷大)。
                for i in range(self.args.n_layer): state[5*i+4] = -1e30 # -infinity
            # 使用token索引self.w.emb.weight,获取词嵌入向量。
            x = self.w.emb.weight[token]
            # 对获取的词嵌入向量x应用层归一化。
            x = self.layer_norm(x, self.w.blocks[0].ln0)
            for i in range(self.args.n_layer):
                att = self.w.blocks[i].att # 获取当前层的注意力参数
                # 这些行使用time_mixing方法对x进行处理,并将结果加到x上。
                x = x + self.time_mixing(self.layer_norm(x, self.w.blocks[i].ln1), state, i,
                    att.time_mix_k, att.time_mix_v, att.time_mix_r, att.time_first, att.time_decay,
                    att.key.weight, att.value.weight, att.receptance.weight, att.output.weight)
                ffn = self.w.blocks[i].ffn # 获取当前层的前馈网络参数。
                # 使用channel_mixing方法对x进行处理,并将结果加到x上。
                x = x + self.channel_mixing(self.layer_norm(x, self.w.blocks[i].ln2), state, i,
                    ffn.time_mix_k, ffn.time_mix_r,
                    ffn.key.weight, ffn.value.weight, ffn.receptance.weight)
            # 对x应用最后的层归一化,并与self.w.head.weight进行矩阵乘法。
            x = self.w.head.weight @ self.layer_norm(x, self.w.ln_out)
            return x.float(), state

从这里可以看到RWKV的state只和层数和嵌入层维度有关系,和序列长度是无关的,这是推理时相比于RNN的核心优点。

采样函数

# 这段代码是一个用于生成随机样本的函数。
# 这是一个函数定义,函数名为 sample_logits,接受三个参数 out、temperature
# 和 top_p,其中 temperature 默认值为 1.0,top_p 默认值为 0.8。
def sample_logits(out, temperature=1.0, top_p=0.8):
    # 这行代码使用 softmax 函数对 out 进行操作,将输出转换为概率分布。
    # dim=-1 表示在最后一个维度上进行 softmax 操作。.numpy() 将结果转换为 NumPy 数组。
    probs = F.softmax(out, dim=-1).numpy()
    # 这行代码使用 NumPy 的 np.sort 函数对概率分布进行排序,
    # 并通过 [::-1] 实现降序排列。结果保存在 sorted_probs 变量中。
    sorted_probs = np.sort(probs)[::-1]
    # 这行代码计算累积概率,使用 NumPy 的 np.cumsum 函数对 sorted_probs
    # 进行累加操作。结果保存在 cumulative_probs 变量中。
    cumulative_probs = np.cumsum(sorted_probs)
    # 这行代码通过比较 cumulative_probs 是否大于 top_p 来找到概率分布中的截断点。
    # np.argmax 返回第一个满足条件的索引,float() 将其转换为浮点数并保存在 cutoff 变量中。
    cutoff = float(sorted_probs[np.argmax(cumulative_probs > top_p)])
    # 这行代码将低于 cutoff 的概率值设为 0,即将概率分布中小于截断点的概率置零。
    probs[probs < cutoff] = 0
    # 这段代码根据 temperature 的取值对概率分布进行调整。
    # 如果 temperature 不等于 1.0,则将概率分布的每个元素取倒数的 1.0 / temperature 次幂。
    if temperature != 1.0:
        probs = probs.pow(1.0 / temperature)
    # 这行代码将概率分布归一化,确保所有概率的总和为 1。
    probs = probs / np.sum(probs)
    # 这行代码使用 np.random.choice 函数根据概率分布 probs 生成一个随机样本,
    # a=len(probs) 表示可选的样本范围为 probs 的长度,p=probs 表示每个样本被选中的概率。
    out = np.random.choice(a=len(probs), p=probs)
    # 函数返回生成的随机样本。
    return out

这个函数的作用是根据给定的概率分布 out 生成一个随机样本,通过调整温度 temperature 和顶部概率 top_p 来控制生成的样本的多样性和稳定性。

生成文本的流程

# 打印使用 CPU 加载模型的信息,其中 args.MODEL_NAME 是模型名称。
print(f'\nUsing CPU. Loading {args.MODEL_NAME} ...')
# 创建一个名为 model 的 RWKV_RNN 模型实例,参数为 args。
model = RWKV_RNN(args)
# 打印预处理上下文信息的提示,提示使用的是较慢的版本。然后初始化 init_state 为 None。
print(f'\nPreprocessing context (slow version. see v2/rwkv/model.py for fast version)')
init_state = None
# 对上下文进行分词编码,并使用模型的 forward 方法逐个处理分词编码的 tokens,
# 将结果保存在 init_out 和 init_state 中。
for token in tokenizer.encode(context).ids:
    init_out, init_state = model.forward(token, init_state)
# 使用循环进行多次试验(NUM_TRIALS 次)。
for TRIAL in range(NUM_TRIALS):
    # 在每次试验的开始打印试验信息和上下文。创建一个空列表 all_tokens 用于保存生成的 tokens。
    print(f'\n\n--[ Trial {TRIAL} ]-----------------', context, end="")
    all_tokens = []
    # 初始化变量 out_last 为 0,out 和 state 分别为 init_out 和 init_state 的克隆。
    out_last = 0
    out, state = init_out.clone(), init_state.clone()
    # 在每个试验中,使用循环生成 LENGTH_PER_TRIAL 个 tokens。
    for i in range(LENGTH_PER_TRIAL):
        # 调用 sample_logits 函数生成一个随机 token,并将其添加到 all_tokens 列表中。
        token = sample_logits(out, TEMPERATURE, TOP_P)
        all_tokens += [token]
        # 使用 tokenizer.decode 将 all_tokens[out_last:] 解码为文本,
        # 并检查解码结果是否包含无效的 utf-8 字符('\ufffd')。如果结果有效,则将其打印出来。
        tmp = tokenizer.decode(all_tokens[out_last:])
        if '\ufffd' not in tmp: # only print when we have a valid utf-8 string
            print(tmp, end="", flush=True)
            out_last = i + 1
        # 调用模型的 forward 方法,将生成的 token 和当前的状态传递给模型,获取更新的 out 和 state。
        out, state = model.forward(token, state)
print('\n')

这段代码的目的是使用 RWKV_RNN 模型生成文本,模型通过 sample_logits 函数生成随机 token,然后将其传递给模型进行预测,并根据预测结果生成下一个 token,不断重复这个过程直到生成指定数量的 tokens。生成的文本会被打印出来。

0x3. ChatRWKV v2聊天系统逻辑实现解析

ChatRWKV的README中提到v2版本实现了一些新功能,建议我们使用,v2版本的代码在 github.com/BlinkDL/Chat 。我们这一节就对这部分的代码做一个解读。

chat.py解析

github.com/BlinkDL/Chat是ChatRWKV v2的核心实现,我们直接来看这个文件。

########################################################################################################
# The RWKV Language Model - https://github.com/BlinkDL/RWKV-LM
########################################################################################################
# os、copy、types、gc、sys:这些是Python标准库,
# 用于操作系统功能、对象复制、类型管理、垃圾回收和系统特定的参数和函数。
import os, copy, types, gc, sys
current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(f'{current_path}/../rwkv_pip_package/src')
import numpy as np
# prompt_toolkit中的prompt:用于构建命令行界面的库。
from prompt_toolkit import prompt
# 脚本尝试根据传递给脚本的命令行参数设置CUDA_VISIBLE_DEVICES环境变量。
try:
    os.environ["CUDA_VISIBLE_DEVICES"] = sys.argv[1]
except:
    pass
# 调用np.set_printoptions()函数设置NumPy数组的打印选项。它配置精度、抑制小值和输出行的最大宽度。
np.set_printoptions(precision=4, suppress=True, linewidth=200)
args = types.SimpleNamespace()
# 脚本打印有关ChatRWKV版本的信息。
print('\n\nChatRWKV v2 https://github.com/BlinkDL/ChatRWKV')
import torch
# 针对PyTorch设置了几个配置,以优化其在GPU上的性能。
torch.backends.cudnn.benchmark = True
torch.backends.cudnn.allow_tf32 = True
torch.backends.cuda.matmul.allow_tf32 = True
# 有一些被注释的行代表不同的设置,可以尝试优化性能。这些设置与PyTorch的即时编译(JIT)和融合有关。
# Tune these below (test True/False for all of them) to find the fastest setting:
# torch._C._jit_set_profiling_executor(True)
# torch._C._jit_set_profiling_mode(True)
# torch._C._jit_override_can_fuse_on_cpu(True)
# torch._C._jit_override_can_fuse_on_gpu(True)
# torch._C._jit_set_texpr_fuser_enabled(False)
# torch._C._jit_set_nvfuser_enabled(False)
########################################################################################################
#
# 有一些注释解释了不同的模型精度选项(fp16、fp32、bf16、xxxi8)及其影响。
# fp16 = good for GPU (!!! DOES NOT support CPU !!!)
# fp32 = good for CPU
# bf16 = less accuracy, supports some CPUs
# xxxi8 (example: fp16i8) = xxx with int8 quantization to save 50% VRAM/RAM, slightly less accuracy
#
# Read https://pypi.org/project/rwkv/ for Strategy Guide
#
########################################################################################################

这段代码设置了必要的依赖项,配置了GPU使用环境,并提供了有关RWKV语言模型及其精度选项的信息。

# 这个变量用于设置模型推理的策略。在代码中有几个不同的策略选项被注释掉了,
# 而实际选择的策略是 'cuda fp16',表示使用CUDA加速并使用半精度浮点数进行计算。
# args.strategy = 'cpu fp32'
args.strategy = 'cuda fp16'
# args.strategy = 'cuda:0 fp16 -> cuda:1 fp16'
# args.strategy = 'cuda fp16i8 *10 -> cuda fp16'
# args.strategy = 'cuda fp16i8'
# args.strategy = 'cuda fp16i8 -> cpu fp32 *10'
# args.strategy = 'cuda fp16i8 *10+'
# 这两个变量设置了环境变量。RWKV_JIT_ON 控制是否启用即时编译(JIT),
# RWKV_CUDA_ON 控制是否编译CUDA内核。在代码中,RWKV_CUDA_ON 被设置为0,即不编译CUDA内核。
os.environ["RWKV_JIT_ON"] = '1' # '1' or '0', please use torch 1.13+ and benchmark speed
os.environ["RWKV_CUDA_ON"] = '0' # '1' to compile CUDA kernel (10x faster), requires c++ compiler & cuda libraries
# 这个变量设置了聊天系统使用的语言,可以选择英语('English')、中文('Chinese')或日语('Japanese')。
CHAT_LANG = 'English' # English // Chinese // more to come
# 这个变量设置了模型的名称和路径。根据选择的语言不同,会有不同的模型名称被设置。
# 模型名称指定了模型文件的位置,以及其他一些相关信息。
# Download RWKV models from https://huggingface.co/BlinkDL
# Use '/' in model path, instead of '\'
# Use convert_model.py to convert a model for a strategy, for faster loading & saves CPU RAM
if CHAT_LANG == 'English':
    args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-raven/RWKV-4-Raven-14B-v12-Eng98%-Other2%-20230523-ctx8192'
    # args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-raven/RWKV-4-Raven-7B-v12-Eng98%-Other2%-20230521-ctx8192'
    # args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-pile-14b/RWKV-4-Pile-14B-20230313-ctx8192-test1050'
elif CHAT_LANG == 'Chinese': # Raven系列可以对话和 +i 问答。Novel系列是小说模型,请只用 +gen 指令续写。
    args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-raven/RWKV-4-Raven-7B-v12-Eng49%-Chn49%-Jpn1%-Other1%-20230530-ctx8192'
    # args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-world/RWKV-4-World-CHNtuned-3B-v1-20230625-ctx4096'
    # args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-novel/RWKV-4-Novel-7B-v1-ChnEng-20230426-ctx8192'
elif CHAT_LANG == 'Japanese':
    # args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-raven/RWKV-4-Raven-14B-v8-EngAndMore-20230408-ctx4096'
    args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-raven/RWKV-4-Raven-7B-v10-Eng89%-Jpn10%-Other1%-20230420-ctx4096'
# -1.py for [User & Bot] (Q&A) prompt
# -2.py for [Bob & Alice] (chat) prompt
# 这个变量设置了模型的名称和路径。根据选择的语言不同,会有不同的模型名称被设置。
# 模型名称指定了模型文件的位置,以及其他一些相关信息。
PROMPT_FILE = f'{current_path}/prompt/default/{CHAT_LANG}-2.py'
# 代码中还包含了一些其他参数的设置,如聊天长度的限制、生成文本的参数(温度、top-p值等)、重复惩罚等。
CHAT_LEN_SHORT = 40
CHAT_LEN_LONG = 150
FREE_GEN_LEN = 256
# For better chat & QA quality: reduce temp, reduce top-p, increase repetition penalties
# Explanation: https://platform.openai.com/docs/api-reference/parameter-details
# 这个变量用于控制生成文本的温度。通过调整温度值,可以控制生成文本的随机性和多样性。
# 在代码中设置为1.2,表示较高的温度,可以增加生成文本的多样性。
GEN_TEMP = 1.2 # It could be a good idea to increase temp when top_p is low
# 这个变量用于控制生成文本的top-p值。Top-p是一种生成文本的策略,
# 它限制了生成文本中概率最高的单词的累积概率。
# 通过减小top-p值,可以提高生成文本的准确性和一致性。在代码中设置为0.5,表示较低的top-p值。
GEN_TOP_P = 0.5 # Reduce top_p (to 0.5, 0.2, 0.1 etc.) for better Q&A accuracy (and less diversity)
# 这两个变量分别控制生成文本中重复内容的惩罚权重。
# GEN_alpha_presence 控制了存在性惩罚的权重,即生成文本中重复内容的惩罚程度。
# GEN_alpha_frequency 控制了频率惩罚的权重,即生成文本中连续重复内容的惩罚程度。
# 在代码中,它们都被设置为0.4。
GEN_alpha_presence = 0.4 # Presence Penalty
GEN_alpha_frequency = 0.4 # Frequency Penalty
# 这个变量控制了重复惩罚的衰减率。通过减小衰减率,可以使重复惩罚在生成文本的后续部分逐渐减弱。
# 在代码中设置为0.996。
GEN_penalty_decay = 0.996
# 这个变量设置了一些标点符号,用于表示要避免在生成文本中重复的内容。
# 在代码中,它包含了中文的逗号、冒号、问号和感叹号。
AVOID_REPEAT = ',:?!'
# 这个变量用于将输入分成多个块,以节省显存(VRAM)。在代码中设置为256。
CHUNK_LEN = 256 # split input into chunks to save VRAM (shorter -> slower)
# 这个变量包含了模型的名称和路径。根据代码中的注释,可以看到有几个不同的模型路径被设置。
# 根据具体情况,会选择其中一个模型路径。
# args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-world/RWKV-4-World-CHNtuned-0.1B-v1-20230617-ctx4096'
# args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-world/RWKV-4-World-CHNtuned-0.4B-v1-20230618-ctx4096'
# args.MODEL_NAME = '/fsx/BlinkDL/HF-MODEL/rwkv-4-world/RWKV-4-World-3B-v1-20230619-ctx4096'
if args.MODEL_NAME.endswith('/'): # for my own usage
    if 'rwkv-final.pth' in os.listdir(args.MODEL_NAME):
        args.MODEL_NAME = args.MODEL_NAME + 'rwkv-final.pth'
    else:
        latest_file = sorted([x for x in os.listdir(args.MODEL_NAME) if x.endswith('.pth')], key=lambda x: os.path.getctime(os.path.join(args.MODEL_NAME, x)))[-1]
        args.MODEL_NAME = args.MODEL_NAME + latest_file

这段代码相当于配置文件,来设置RWKV聊天系统的运行参数和模型信息。根据选择的语言不同,会加载相应的模型,并设置相应的参数。

########################################################################################################
# 这行代码用于打印一个字符串,包含了 CHAT_LANG、args.strategy 和 PROMPT_FILE 的值。
# 它会在控制台输出当前的语言、策略和提示文件的信息。
print(f'\n{CHAT_LANG} - {args.strategy} - {PROMPT_FILE}')
# 导入 RWKV 模型
from rwkv.model import RWKV
# 这行代码导入了 PIPELINE 工具,用于处理模型的输入和输出。
from rwkv.utils import PIPELINE
# 用于加载提示文件的内容并返回相应的变量。
def load_prompt(PROMPT_FILE):
    # 该函数首先创建了一个空的字典 variables,然后使用 open 函数打开 PROMPT_FILE 文件。
    variables = {}
    # 下来,使用 exec 函数将文件内容编译并执行,将结果存储在 variables 字典中。
    with open(PROMPT_FILE, 'rb') as file:
        exec(compile(file.read(), PROMPT_FILE, 'exec'), variables)
    # 然后,从 variables 字典中获取了 user、bot、interface 和 init_prompt 的值。
    # init_prompt 被处理为一个列表,去除了多余的空格和换行符,并在开头和结尾添加了换行符。
    # 最后,函数返回获取的变量。
    user, bot, interface, init_prompt = variables['user'], variables['bot'], variables['interface'], variables['init_prompt']
    init_prompt = init_prompt.strip().split('\n')
    for c in range(len(init_prompt)):
        init_prompt[c] = init_prompt[c].strip().strip('\u3000').strip('\r')
    init_prompt = '\n' + ('\n'.join(init_prompt)).strip() + '\n\n'
    return user, bot, interface, init_prompt
# Load Model
# 这行代码用于打印一个字符串,指示正在加载模型,并输出 args.MODEL_NAME 的值。
print(f'Loading model - {args.MODEL_NAME}')
# 这行代码创建了一个 RWKV 模型对象,使用指定的模型路径 args.MODEL_NAME 和策略 args.strategy。
model = RWKV(model=args.MODEL_NAME, strategy=args.strategy)
# 根据 args.MODEL_NAME 的值,选择了不同的分词器和特殊标记。
# 如果模型路径中包含 'world/' 或 '-World-',则使用 "rwkv_vocab_v20230424" 的分词器,
# 并设置了 END_OF_TEXT 和 END_OF_LINE 的值。
# 否则,使用 current_path(当前路径)和 "20B_tokenizer.json" 的分词器,
# 并设置了 END_OF_TEXT、END_OF_LINE 和 END_OF_LINE_DOUBLE 的值。
if 'world/' in args.MODEL_NAME or '-World-' in args.MODEL_NAME:
    pipeline = PIPELINE(model, "rwkv_vocab_v20230424")
    END_OF_TEXT = 0
    END_OF_LINE = 11
else:
    pipeline = PIPELINE(model, f"{current_path}/20B_tokenizer.json")
    END_OF_TEXT = 0
    END_OF_LINE = 187
    END_OF_LINE_DOUBLE = 535
# pipeline = PIPELINE(model, "cl100k_base")
# END_OF_TEXT = 100257
# END_OF_LINE = 198
# 这行代码创建了一个空的列表 model_tokens,用于存储模型的输入token。
model_tokens = []
model_state = None
# 这行代码创建了一个空的列表 AVOID_REPEAT_TOKENS,用于存储避免重复的标记。
AVOID_REPEAT_TOKENS = []
for i in AVOID_REPEAT:
    # 在循环内部,将当前元素 i 使用 pipeline.encode 函数进行编码,
    # 并将结果添加到 AVOID_REPEAT_TOKENS 列表中。
    # 这样,AVOID_REPEAT_TOKENS 列表中存储了避免重复的token的编码。
    dd = pipeline.encode(i)
    assert len(dd) == 1
    AVOID_REPEAT_TOKENS += dd

这段代码的主要功能是加载和配置RWKV模型,并准备生成文本所需的参数和工具。继续解析:

########################################################################################################
# 这是一个函数定义,用于以RNN模式运行RWKV模型生成文本。
def run_rnn(tokens, newline_adj = 0):
    # 这行代码声明在函数内部使用全局变量 model_tokens 和 model_state。
    global model_tokens, model_state
    # 将输入的token列表转换为整数类型。
    tokens = [int(x) for x in tokens]
    # 将输入的标记列表添加到全局变量 model_tokens 中。
    model_tokens += tokens
    # print(f'### model ###\n{tokens}\n[{pipeline.decode(model_tokens)}]')
    # 当token列表的长度大于0时,执行以下操作:
    while len(tokens) > 0:
        # 使用模型的前向传播函数 model.forward 对token列表的前
        # CHUNK_LEN 个token进行推理,并更新模型状态。
        out, model_state = model.forward(tokens[:CHUNK_LEN], model_state)
        # 将token列表更新为剩余的token。
        tokens = tokens[CHUNK_LEN:]
    # 将输出概率向量中的换行符标记 END_OF_LINE 的概率增加 newline_adj,用于调整换行的概率。
    out[END_OF_LINE] += newline_adj # adjust \n probability
    # 如果模型最后一个标记在避免重复标记列表 AVOID_REPEAT_TOKENS 中,执行以下操作:
    if model_tokens[-1] in AVOID_REPEAT_TOKENS:
        # 将输出概率向量中模型最后一个标记对应的概率设置为一个极小的值,用于避免模型生成重复的标记。
        out[model_tokens[-1]] = -999999999
    return out
all_state = {}
# 这是一个函数定义,用于保存模型状态和token列表。
def save_all_stat(srv, name, last_out):
    # 创建保存状态的键名。
    n = f'{name}_{srv}'
    all_state[n] = {} # 创建一个空的字典,用于保存模型状态和token列表。
    all_state[n]['out'] = last_out # 将最后的输出概率向量保存到字典中。
    all_state[n]['rnn'] = copy.deepcopy(model_state) # 将深拷贝后的模型状态保存到字典中。
    all_state[n]['token'] = copy.deepcopy(model_tokens) # 将深拷贝后的token列表保存到字典中。
# 这是一个函数定义,用于加载保存的模型状态和标记列表。
def load_all_stat(srv, name):
    # 这行代码声明在函数内部使用全局变量 model_tokens 和 model_state。
    global model_tokens, model_state
    # 获取保存状态的键名。
    n = f'{name}_{srv}'
    # 将保存的模型状态深拷贝给全局变量 model_state。
    model_state = copy.deepcopy(all_state[n]['rnn'])
    # 将保存的token列表深拷贝给全局变量 model_tokens。
    model_tokens = copy.deepcopy(all_state[n]['token'])
    return all_state[n]['out']
# Model only saw '\n\n' as [187, 187] before, but the tokenizer outputs [535] for it at the end
# 这是一个函数定义,用于修复token列表。
def fix_tokens(tokens):
    # 根据模型路径是否包含 'world/' 或 '-World-',执行以下操作:
    if 'world/' in args.MODEL_NAME or '-World-' in args.MODEL_NAME:
        # 如果是,则返回原始的标记列表 tokens,无需修复。
        return tokens
    # 如果不是,则检查标记列表的最后一个标记是否为 END_OF_LINE_DOUBLE,
    # 如果是,则将标记列表中的最后一个标记替换为 END_OF_LINE 重复两次。
    if len(tokens) > 0 and tokens[-1] == END_OF_LINE_DOUBLE:
        tokens = tokens[:-1] + [END_OF_LINE, END_OF_LINE]
    return tokens
########################################################################################################

这段代码定义了以RNN模式运行RWKV的函数以及保存和恢复模型状态的函数,最后还定义了一个修复tokens的工具函数用于对world模型和其它模型分别进行处理,继续解析:

########################################################################################################
# Run inference
print(f'\nRun prompt...') # 打印提示信息 "Run prompt..."。
# 调用 load_prompt 函数加载提示文件,将返回的用户、机器人、界面和初始提示内容
# 分别赋值给变量 user、bot、interface 和 init_prompt。
user, bot, interface, init_prompt = load_prompt(PROMPT_FILE)
# 调用 fix_tokens 函数修复初始提示内容的标记列表,并将修复后的标记列表传递给 run_rnn 函数进行推理。
# 将生成的输出概率向量赋值给变量 out。
out = run_rnn(fix_tokens(pipeline.encode(init_prompt)))
# 调用 save_all_stat 函数保存模型状态和标记列表,键名为 'chat_init',值为 out。
save_all_stat('', 'chat_init', out)
# 执行垃圾回收和清空GPU缓存的操作。
gc.collect()
torch.cuda.empty_cache()
# 创建一个服务器列表 srv_list,其中包含一个名为 'dummy_server' 的服务器。
srv_list = ['dummy_server']
# 遍历服务器列表,对于每个服务器,调用 save_all_stat 函数保存模型状态和token列表,
# 键名包含服务器名和 'chat',值为 out。
for s in srv_list:
    save_all_stat(s, 'chat', out)
# 定义一个名为 reply_msg 的函数,该函数接受一个参数 msg,并打印机器人、界面和回复消息。
def reply_msg(msg):
    print(f'{bot}{interface} {msg}\n')
# 这段代码定义了一个名为 on_message 的函数,用于处理接收到的消息。
def on_message(message):
    # 声明在函数内部使用全局变量 model_tokens、model_state、user、bot、interface 和 init_prompt。
    global model_tokens, model_state, user, bot, interface, init_prompt
    # 将字符串 'dummy_server' 赋值给变量 srv。
    srv = 'dummy_server'
    # 将接收到的消息中的转义字符 '\\n' 替换为换行符 '\n',并去除首尾空白字符,将结果赋值给变量 msg。
    msg = message.replace('\\n','\n').strip()
    x_temp = GEN_TEMP
    x_top_p = GEN_TOP_P
    # 如果消息中包含 -temp=,执行以下操作:
    # a. 从消息中提取 -temp= 后面的值,并将其转换为浮点数类型赋值给 x_temp。
    # 从消息中移除 -temp= 部分。
    if ("-temp=" in msg):
        x_temp = float(msg.split("-temp=")[1].split(" ")[0])
        msg = msg.replace("-temp="+f'{x_temp:g}', "")
        # print(f"temp: {x_temp}")
    if ("-top_p=" in msg):
        x_top_p = float(msg.split("-top_p=")[1].split(" ")[0])
        msg = msg.replace("-top_p="+f'{x_top_p:g}', "")
        # print(f"top_p: {x_top_p}")
    # 如果 x_temp 小于等于 0.2,将其设置为 0.2。
    if x_temp <= 0.2:
        x_temp = 0.2
    # 如果 x_temp 大于等于 5,将其设置为 5。
    if x_temp >= 5:
        x_temp = 5
    # 如果 x_top_p 小于等于 0,将其设置为 0。
    if x_top_p <= 0:
        x_top_p = 0
    # 去除消息首尾空白字符。
    msg = msg.strip()
    # 如果消息等于 '+reset':
    if msg == '+reset':
        # 加载保存的初始模型状态并将其保存为 out。
        out = load_all_stat('', 'chat_init')
        # 保存模型状态和token列表。
        save_all_stat(srv, 'chat', out)
        # 调用 reply_msg 函数打印回复消息 "Chat reset."。
        reply_msg("Chat reset.")
        return
    # use '+prompt {path}' to load a new prompt
    # 如果消息以 '+prompt ' 开头
    elif msg[:8].lower() == '+prompt ':
        # 打印 "Loading prompt..."。
        print("Loading prompt...")
        try:
            # 提取消息中 'prompt ' 后面的内容作为提示文件路径,并将其赋值给变量 PROMPT_FILE。
            PROMPT_FILE = msg[8:].strip()
            # 加载提示文件,将返回的用户、机器人、界面和初始提示内容分别赋值给变量
            # user、bot、interface 和 init_prompt。
            user, bot, interface, init_prompt = load_prompt(PROMPT_FILE)
            # 对prompt编码和推理
            out = run_rnn(fix_tokens(pipeline.encode(init_prompt)))
            # 保存模型状态和token列表。
            save_all_stat(srv, 'chat', out)
            # 打印 "Prompt set up."。
            print("Prompt set up.")
            gc.collect()
            torch.cuda.empty_cache()
        except:
            # 捕获异常,打印 "Path error."。
            print("Path error.")
    # 如果消息以 '+gen '、'+i '、'+qa '、'+qq '、'+++' 或 '++' 开头,执行以下操作:
    elif msg[:5].lower() == '+gen ' or msg[:3].lower() == '+i ' or msg[:4].lower() == '+qa ' or msg[:4].lower() == '+qq ' or msg.lower() == '+++' or msg.lower() == '++':
        if msg[:5].lower() == '+gen ':
            # 提取消息中 'gen ' 后面的内容作为新的提示内容,并将其赋值给变量 new。
            new = '\n' + msg[5:].strip()
            # print(f'### prompt ###\n[{new}]')
            # 将模型状态和标记列表重置为空。
            model_state = None
            model_tokens = []
            # 运行RNN模型进行推理,生成回复的输出概率向量,并将其保存为 out。
            out = run_rnn(pipeline.encode(new))
            # 保存模型状态和token列表。
            save_all_stat(srv, 'gen_0', out)
        elif msg[:3].lower() == '+i ':
            # 提取消息中 'i ' 后面的内容作为新的指令,并将其赋值给变量 msg。
            msg = msg[3:].strip().replace('\r\n','\n').replace('\n\n','\n')
            # 替换指令中的换行符,将 '\r\n' 替换为 '\n',将连续的两个换行符 '\n\n' 替换为单个换行符 '\n'。
            # 构建新的提示内容 new,包括指令和响应模板。
            new = f'''
Below is an instruction that describes a task. Write a response that appropriately completes the request.
# Instruction:
{msg}
# Response:
'''
            # print(f'### prompt ###\n[{new}]')
            # 将模型状态和token列表重置为空。
            model_state = None
            model_tokens = []
            # 运行RNN模型进行推理,生成回复的输出概率向量,并将其保存为 out。
            out = run_rnn(pipeline.encode(new))
            # 保存模型状态和token列表。
            save_all_stat(srv, 'gen_0', out)
        elif msg[:4].lower() == '+qq ':
            # 提取消息中 'qq ' 后面的内容作为新的问题,构建新的提示内容 new,包括问题和回答模板。
            new = '\nQ: ' + msg[4:].strip() + '\nA:'
            # print(f'### prompt ###\n[{new}]')
            # 将模型状态和token列表重置为空。
            model_state = None
            model_tokens = []
            # 运行RNN模型进行推理,生成回复的输出概率向量,并将其保存为 out。
            out = run_rnn(pipeline.encode(new))
            # 保存模型状态和token列表。
            save_all_stat(srv, 'gen_0', out)
        elif msg[:4].lower() == '+qa ':
            # 加载保存的初始模型状态并将其保存为 out。
            out = load_all_stat('', 'chat_init')
            # 提取消息中 'qa ' 后面的内容作为真实消息,并将其赋值给变量 real_msg。
            real_msg = msg[4:].strip()
            # 构建新的提示内容 new,包括用户、界面、真实消息和机器人。
            new = f"{user}{interface} {real_msg}\n\n{bot}{interface}"
            # print(f'### qa ###\n[{new}]')
            # 运行RNN模型进行推理,生成回复的输出概率向量,并将其保存为 out。
            out = run_rnn(pipeline.encode(new))
            # 保存模型状态和token列表。
            save_all_stat(srv, 'gen_0', out)
        elif msg.lower() == '+++':
            try:
                # 加载保存的模型状态 gen_1 并将其保存为 out。
                out = load_all_stat(srv, 'gen_1')
                # 保存模型状态和token列表。
                save_all_stat(srv, 'gen_0', out)
            except:
                return
        elif msg.lower() == '++':
            try:
                # 加载保存的模型状态 gen_0 并将其保存为 out。
                out = load_all_stat(srv, 'gen_0')
            except:
                return
        # 将变量 begin 设置为 model_tokens 的长度。
        begin = len(model_tokens)
        # 将变量 out_last 设置为 begin。
        out_last = begin
        # 创建一个空字典 occurrence。
        occurrence = {}
        # 循环 FREE_GEN_LEN+100 次,其中 FREE_GEN_LEN 是一个常量,代表自由生成的长度。
        for i in range(FREE_GEN_LEN+100):
            # 遍历字典 occurrence 中的键。
            for n in occurrence:
                # 将 out[n] 减去一个计算得到的重复惩罚项。
                out[n] -= (GEN_alpha_presence + occurrence[n] * GEN_alpha_frequency)
            # 使用 pipeline.sample_logits 函数根据概率向量 out 生成一个标记,并将其赋值给 token。
            token = pipeline.sample_logits(
                out,
                temperature=x_temp,
                top_p=x_top_p,
            )
            # 如果 token 等于 END_OF_TEXT,跳出内层循环。
            if token == END_OF_TEXT:
                break
            # 遍历字典 occurrence 中的键。将 occurrence[xxx] 乘以一个常量 GEN_penalty_decay
            for xxx in occurrence:
                occurrence[xxx] *= GEN_penalty_decay
            # 如果 token 不在 occurrence 中,将 occurrence[token] 设置为 1。
            if token not in occurrence:
                occurrence[token] = 1
            else:
            # 否则+1,表示这个token重复次数+1
                occurrence[token] += 1
            # 如果 msg[:4].lower() == '+qa ',调用 run_rnn 函数,
            # 传递 [token] 作为参数,并将返回值赋值给变量 out。
            if msg[:4].lower() == '+qa ':# or msg[:4].lower() == '+qq ':
                out = run_rnn([token], newline_adj=-2)
            else:
                # 调用 run_rnn 函数,传递 [token] 作为参数,并将返回值赋值给变量 out。
                out = run_rnn([token])
            # 使用 pipeline.decode 函数将 model_tokens[out_last:] 解码为字符串,并将结果赋值给 xxx。
            xxx = pipeline.decode(model_tokens[out_last:])
            # 如果字符串 '\ufffd' 不在 xxx 中
            if '\ufffd' not in xxx: # avoid utf-8 display issues
                # 打印 xxx,并刷新输出缓冲区。
                print(xxx, end='', flush=True)
                # 将 out_last 设置为 begin + i + 1。
                out_last = begin + i + 1
                # 如果 i 大于等于 FREE_GEN_LEN,跳出外层循环。
                if i >= FREE_GEN_LEN:
                    break
        print('\n')
        # send_msg = pipeline.decode(model_tokens[begin:]).strip()
        # print(f'### send ###\n[{send_msg}]')
        # reply_msg(send_msg)
        # 调用 save_all_stat 函数,将参数 srv、'gen_1' 和 out 传递给它。
        save_all_stat(srv, 'gen_1', out)
    else:
        # 如果 msg.lower() == '+'
        if msg.lower() == '+':
            try:
                # 尝试加载状态信息 load_all_stat(srv, 'chat_pre')。
                out = load_all_stat(srv, 'chat_pre')
            except:
                return
        else:
            # 加载状态信息 load_all_stat(srv, 'chat'),并将结果赋值给变量 out。
            out = load_all_stat(srv, 'chat')
            # 对消息 msg 进行处理,去除首尾空格,替换换行符,
            msg = msg.strip().replace('\r\n','\n').replace('\n\n','\n')
            # 并构造新的消息字符串 new,其中包括用户和机器人的标识符。
            new = f"{user}{interface} {msg}\n\n{bot}{interface}"
            # print(f'### add ###\n[{new}]')
            # 调用 run_rnn 函数,传递 pipeline.encode(new) 作为参数,并将返回值赋值给变量 out。
            out = run_rnn(pipeline.encode(new), newline_adj=-999999999)
            # 将生成的状态信息 out 保存为 'chat_pre' 的状态信息。
            save_all_stat(srv, 'chat_pre', out)
        # 这里开始的内容和上一个elif里面的基本一致,就不重复解析了
        begin = len(model_tokens)
        out_last = begin
        print(f'{bot}{interface}', end='', flush=True)
        occurrence = {}
        for i in range(999):
            if i <= 0:
                newline_adj = -999999999
            elif i <= CHAT_LEN_SHORT:
                newline_adj = (i - CHAT_LEN_SHORT) / 10
            elif i <= CHAT_LEN_LONG:
                newline_adj = 0
            else:
                newline_adj = min(3, (i - CHAT_LEN_LONG) * 0.25) # MUST END THE GENERATION
            for n in occurrence:
                out[n] -= (GEN_alpha_presence + occurrence[n] * GEN_alpha_frequency)
            token = pipeline.sample_logits(
                out,
                temperature=x_temp,
                top_p=x_top_p,
            )
            # if token == END_OF_TEXT:
            #     break
            for xxx in occurrence:
                occurrence[xxx] *= GEN_penalty_decay
            if token not in occurrence:
                occurrence[token] = 1
            else:
                occurrence[token] += 1
            out = run_rnn([token], newline_adj=newline_adj)
            out[END_OF_TEXT] = -999999999  # disable <|endoftext|>
            xxx = pipeline.decode(model_tokens[out_last:])
            if '\ufffd' not in xxx: # avoid utf-8 display issues
                print(xxx, end='', flush=True)
                out_last = begin + i + 1
            send_msg = pipeline.decode(model_tokens[begin:])
            if '\n\n' in send_msg:
                send_msg = send_msg.strip()
                break
            # send_msg = pipeline.decode(model_tokens[begin:]).strip()
            # if send_msg.endswith(f'{user}{interface}'): # warning: needs to fix state too !!!
            #     send_msg = send_msg[:-len(f'{user}{interface}')].strip()
            #     break
            # if send_msg.endswith(f'{bot}{interface}'):
            #     send_msg = send_msg[:-len(f'{bot}{interface}')].strip()
            #     break
        # print(f'{model_tokens}')
        # print(f'[{pipeline.decode(model_tokens)}]')
        # print(f'### send ###\n[{send_msg}]')
        # reply_msg(send_msg)
        save_all_stat(srv, 'chat', out)
########################################################################################################

总的来看,这段代码是一个循环,用于ChatRWKV系统生成回复消息和更新状态信息。它根据输入消息和模型的状态信息进行一个RNN模式的推理,生成一个回复的token和一个新的状态,然后将生成的回复显示出来,生成的状态则可以在下一次生成中继续使用。代码还包括处理特殊命令以及加载和保存状态信息的逻辑。

0x3. ChatRWKV v2聊天系统指南

  • 直接说话:聊天,用 + 换个回答
  • reset: 通过发送+reset消息,您可以重置聊天。
  • 加载新的提示: 使用+prompt {path}可以加载一个新的提示,其中{path}是提示文件的路径。
  • 消息类型:
    • +gen {text}: 基于{text}生成新的内容。
    • +i {instruction}: 根据给定的指令{instruction}产生一个响应。
    • +qq {question}: 对于给定的问题{question},生成一个答案。
    • +qa {text}: 将{text}作为一个问题,并生成一个答案。
    • +++: 继续写下去。
    • ++: 换个写法。

除了这些指令之外,还可以调整生成参数:

  • -temp=: 调整生成的温度。温度影响生成文本的随机性。较高的温度将导致更多的随机输出,而较低的温度将导致更确定的输出。
  • -top_p=: 调整Top-P采样。Top-P采样是选择词汇中概率最高的一部分词进行采样的方法。

0x4. 总结

这篇文章还有一些ChatRWKV v2系统的模型实现部分,tokenizer部分都没有解析到,但目前篇幅已经比较多了,希望留到下次解析。enjoy ChatRWKV v2!

]]>
Sat, 02 Sep 2023 05:43:30 GMT https://zhuanlan.zhihu.com/p/653327189 https://zhuanlan.zhihu.com/p/653327189
<![CDATA[PENG Bo赞同了文章: MLC-LLM 部署RWKV World系列模型实战(3B模型Mac M2解码可达26tokens/s)]]> 0x0. 前言

我的 ChatRWKV 学习笔记和使用指南 这篇文章是学习RWKV的第一步,然后学习了一下之后决定自己应该做一些什么。所以就在RWKV社区看到了这个将RWKV World系列模型通过MLC-LLM部署在各种硬件平台的需求,然后我就开始了解MLC-LLM的编译部署流程和RWKV World模型相比于MLC-LLM已经支持的Raven系列模型的特殊之处。

MLC-LLM的编译部署流程在MLC-LLM的官方文档已经比较详细了,但这部分有一些隐藏的坑点需要你去发现,比如现在要支持的RWKV-World模型它的Tokenizer是自定义的,并不是Huggingface的格式,这就导致我们不能使用MLC-LLM去直接编译这个模型,也不能使用预编译好的MLC-LLM二进制库去运行这个模型了。另外,在编译MLC-LLM仓库之前我们需要先编译Relax仓库而不是原始的TVM仓库,Relax可以认为是TVM的一个fork,在此基础上支持了Relax这个新一代的IR,这部分背景建议读者看一下我这个仓库的相关链接:

github.com/BBuf/tvm_mli

这个仓库已经揽下1.4k star,谢谢读者们支持。

从RWKV社区了解到,RWKV-World系列模型相比于Raven系列,推理代码和模型都是完全一样,不一样的地方主要是tokenizer是自定义的,并且system prompt不同。

在编译Relax的时候需要按需选择自己的编译平台进行编译,编译完之后 MLC-LLM 会通过 TVM_HOME 这个环境变量来感知 Relax 的位置,并且Relax编译时开启的选项要和MLC-LLM编译的选项匹配上,这样才可以在指定平台上进行正确的编译和推理。

在适配 RWKV-World 1.5B时,由于模型比较小对逗号比较敏感,导致第一层就炸了精度,最终挂在sampler里面,这个地方我定位2个晚上,后来mlc-ai官方的冯思远告诉我在 MLC-LLM 里如何逐层打印精度之后,我最终定位到了问题。并且在 RWKV 社区里面了解到了这个现象之前就出现过,那就是1.5B的模型第一层需要用FP32来计算,不然会炸精度,我后续实验了RWKV-4-World 3B/7B,这个现象就没有了。

另外,模型的组织格式也是值得注意的一点,并不是在任意位置编译好模型都可以在运行时被 MLC-LLM 正确发现。我大概花了快一周工作外时间在 MLC-LLM 上来支持 RWKV-World 系列模型,工作内容主要为:

  • 将大缺弦的 github.com/daquexian/fa 仓库中的 RWKV World模型tokenizer实现挂到 mlc-ai 的 tokenizers.cpp 中,作为一个 3rd 库提供给MLC-LLM。合并的PR为:github.com/mlc-ai/token
  • 在上面的基础上,在MLC-LLM中支持 RWKV World系列模型的部署,对齐 World 系列模型的 Prompt ,获得良好的对话效果。分别在 Apple M2和A800显卡上进行了部署和测试。PR为:github.com/mlc-ai/mlc-l ,这个pr还wip,如果你现在要使用的话可以直接切到这个pr对应的分支就可以了。
  • debug到1.5B RWKV World小模型会炸精度的bug,相当于踩了个大坑。

我要特别感谢 mlc-ai 官方的冯思远在我部署过程中提供的支持以及帮我Review让代码合并到 mlc-ai 社区,以及感谢大缺弦的 RWKV World Tokenizer c++实现以及在编译第三方库时帮我解决的一个bug。

以下是MLC-LLM 部署RWKV World系列模型教程,尽量提供大家部署最不踩坑的实践。

效果:

在这里插入图片描述

0x1. 将RWKV-4-World-7B部署在A800上

准备工作

编译Relax

git clone --recursive git@github.com:mlc-ai/relax.git
cd relax
mkdir build
cd build
cp ../cmake/config.cmake ./

然后修改build目录下的config.cmake文件,由于我这里是在A800上面编译,我改了以下设置:

set(USE_CUDA ON)
set(USE_CUTLASS ON)
set(USE_CUBLAS ON)

即启用了CUDA,并开启了2个加速库CUTLASS和CUBLAS。然后在build目录下执行cmake .. && make -j32 即可。

最后可以考虑把Relax添加到PYTHONPATH环境变量里面使得全局可见,在~/.bashrc上输入以下内容:

export TVM_HOME=/bbuf/relax
export PYTHONPATH=$TVM_HOME/python:${PYTHONPATH}

然后source ~/.bashrc即可。

编译和安装MLC-LLM

git clone --recursive git@github.com:mlc-ai/mlc-llm.git
cd mlc-llm/cmake
python3 gen_cmake_config.py

执行python3 gen_cmake_config.py 可以按需选择需要打开的编译选项,比如我这里就选择打开CUDA,CUBLAS,CUTLASS,另外需要注意的是这里的 TVM_HOME 路径需要设置为上面编译的Relax路径。

然后执行下面的操作编译:

cd ..
mkdir build
cp cmake/config.cmake build
cd build
cmake ..
make -j32

这里编译时还需要安装一下rust,按照建议的命令安装即可,编译完成之后即安装上了mlc-llm提供的聊天程序mlc_chat_cli。然后为了做模型转换和量化,我们还需要在mlc-llm目录下执行一下pip install .安装mlc_llm包。

模型转换

模型转换这里基本就是参考这个教程了:mlc.ai/mlc-llm/docs/com

例如我们执行python3 -m mlc_llm.build --hf-path StarRing2022/RWKV-4-World-7B --target cuda --quantization q4f16_1 就可以将RWKV-4-World-7B模型权重量化为4个bit,然后activation还是以FP16的方式存储。


target 则指定我们要在什么平台上去运行,这里会将整个模型构成的图编译成一个动态链接库(也就是TVM的IRModule)供后续的mlc_chat_cli程序(这个是在编译mlc-llm时产生的)调用。

这里默认会在当前目录下新建一个dist/models文件夹来存量化后模型和配置文件以及链接库,转换和量化好之后的模型会存储在当前命令所在目录的dist子目录下(会自动创建),你也可以手动克隆huggingface模型到dist/models文件夹下。量化完之后的模型结构如下:


这里的mlc-chat-config.json指定来模型生成的一些超参数比如top_p,temperature等。

最后在推理之前,我们还需要把最开始准备的tokenizer_model文件拷贝到这个params文件夹中。

执行推理

我们在mlc-llm的上一层文件夹执行下面的命令:

./mlc-llm/build/mlc_chat_cli --model RWKV-4-World-7B-q0f16

RWKV-4-World-7B-q0f16可以换成你量化模型时的名字,加载完并运行system prompt之后你就可以愉快的和RWKV-4-World模型聊天了。


程序有一些特殊的指令来退出,查看速度等等:


性能测试

硬件量化方法速度
A800q0f16prefill: 362.7 tok/s, decode: 72.4 tok/s
A800q4f16_1prefill: 1104.7 tok/s, decode: 122.6 tok/s

这里给2组性能数据,大家感兴趣的话可以测测其它配置。

逐层debug方法

在适配1.5B模型时出现了推理结果nan的现象,可以用mlc-llm/tests/debug/dump_intermediate.py这个文件来对齐输入和tokenizer的结果之后进行debug,可以精准模拟模型推理并打印每一层的中间值,这样我们就可以方便的看到模型是在哪一层出现了nan。

0x2. 将RWKV-4-World-3B部署在Apple M2上

在mac上部署和cuda上部署并没有太大区别,主要是编译relax和mlc-llm的时候编译选项现在要选Metal而不是cuda了。我建议最好是在一个anconda环境里面处理编译的问题,不要用系统自带的python环境。

在编译relax的时候需要同时打开使用Metal和LLVM选项,如果系统没有LLVM可以先用Homebrew装一下。

在mlc-llm中生成config.cmake时使用下面的选项:


编译完并pip install .之后使用下面的命令量化模型:

python3 -m mlc_llm.build --hf-path StarRing2022/RWKV-4-World-3B --target metal --quantization q4f16_1

量化过程中日志如下:

(base) bbuf@MacBook-Pro RWKV % python3 -m mlc_llm.build --hf-path StarRing2022/RWKV-4-World-3B --target metal --quantization q4f16_1
Weights exist at dist/models/RWKV-4-World-3B, skipping download.
Using path "dist/models/RWKV-4-World-3B" for model "RWKV-4-World-3B"
[09:53:08] /Users/bbuf/工作目录/RWKV/relax/src/runtime/metal/metal_device_api.mm:167: Intializing Metal device 0, name=Apple M2
Host CPU dection:
  Target triple: arm64-apple-darwin22.3.0
  Process triple: arm64-apple-darwin22.3.0
  Host CPU: apple-m1
Target configured: metal -keys=metal,gpu -max_function_args=31 -max_num_threads=256 -max_shared_memory_per_block=32768 -max_threads_per_block=1024 -thread_warp_size=32
Host CPU dection:
  Target triple: arm64-apple-darwin22.3.0
  Process triple: arm64-apple-darwin22.3.0
  Host CPU: apple-m1
Automatically using target for weight quantization: metal -keys=metal,gpu -max_function_args=31 -max_num_threads=256 -max_shared_memory_per_block=32768 -max_threads_per_block=1024 -thread_warp_size=32
Start computing and quantizing weights... This may take a while.
Finish computing and quantizing weights.
Total param size: 1.6060066223144531 GB
Start storing to cache dist/RWKV-4-World-3B-q4f16_1/params
[0808/0808] saving param_807
All finished, 51 total shards committed, record saved to dist/RWKV-4-World-3B-q4f16_1/params/ndarray-cache.json
Finish exporting chat config to dist/RWKV-4-World-3B-q4f16_1/params/mlc-chat-config.json
[09:53:40] /Users/bbuf/工作目录/RWKV/relax/include/tvm/topi/transform.h:1076: Warning: Fast mode segfaults when there are out-of-bounds indices. Make sure input indices are in bound
[09:53:41] /Users/bbuf/工作目录/RWKV/relax/include/tvm/topi/transform.h:1076: Warning: Fast mode segfaults when there are out-of-bounds indices. Make sure input indices are in bound
Save a cached module to dist/RWKV-4-World-3B-q4f16_1/mod_cache_before_build.pkl.
Finish exporting to dist/RWKV-4-World-3B-q4f16_1/RWKV-4-World-3B-q4f16_1-metal.so

同样也需要把tokenizer_model文件拷贝到量化后模型文件夹的params目录下,然后执行下面的命令启动聊天程序:

./mlc-llm/build/mlc_chat_cli --model RWKV-4-World-3B-q0f16
KwToPA commented 1 year ago

我自建的又被封了

还是报错 Error message: Failed to extract d_c0 from cookies

官方的也是403

最初被封时,我停了1天,之后解封过1次,几天后再次被封锁IP,到如今都没解封

KwToPA commented 1 year ago

我自建的又被封了

还是报错 Error message: Failed to extract d_c0 from cookies

官方的也是403

最初被封时,我停了1天,之后解封过1次,几天后再次被封锁IP,到如今都没解封

知乎网页版是不是改版了? 界面有些不一样