├── QwenMNN.pro ├── QwenMNN.pro.user ├── README.md ├── android ├── AndroidManifest.xml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── res │ ├── drawable-hdpi │ └── icon.png │ ├── drawable-ldpi │ └── icon.png │ ├── drawable-mdpi │ └── icon.png │ └── values │ └── libs.xml ├── main.cpp ├── pic ├── inference.png ├── llm-export.png ├── llm.png ├── quant.png ├── qwen.png └── structure.png ├── widget.cpp ├── widget.h ├── widget.ui └── windows └── Qwen0.5B.cpp /QwenMNN.pro: -------------------------------------------------------------------------------- 1 | QT += core gui 2 | 3 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 4 | 5 | CONFIG += c++17 6 | 7 | # The following define makes your compiler emit warnings if you use 8 | # any Qt feature that has been marked deprecated (the exact warnings 9 | # depend on your compiler). Please consult the documentation of the 10 | # deprecated API in order to know how to port your code away from it. 11 | DEFINES += QT_DEPRECATED_WARNINGS 12 | 13 | # You can also make your code fail to compile if it uses deprecated APIs. 14 | # In order to do so, uncomment the following line. 15 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 16 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 17 | 18 | SOURCES += \ 19 | main.cpp \ 20 | widget.cpp 21 | 22 | HEADERS += \ 23 | widget.h 24 | 25 | FORMS += \ 26 | widget.ui 27 | 28 | # Default rules for deployment. 29 | qnx: target.path = /tmp/$${TARGET}/bin 30 | else: unix:!android: target.path = /opt/$${TARGET}/bin 31 | !isEmpty(target.path): INSTALLS += target 32 | 33 | DISTFILES += \ 34 | android/AndroidManifest.xml \ 35 | android/build.gradle \ 36 | android/gradle/wrapper/gradle-wrapper.jar \ 37 | android/gradle/wrapper/gradle-wrapper.properties \ 38 | android/gradlew \ 39 | android/gradlew.bat \ 40 | android/res/values/libs.xml 41 | 42 | ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android 43 | 44 | unix:!macx: LIBS += -L$$PWD/../../mnn-llm-android/libs/ -lMNN 45 | INCLUDEPATH += $$PWD/../../mnn-llm-android/include 46 | DEPENDPATH += $$PWD/../../mnn-llm-android/include 47 | unix:!macx: LIBS += -L$$PWD/../../mnn-llm-android/libs/ -lMNN_Express 48 | unix:!macx: LIBS += -L$$PWD/../../mnn-llm-android/android_build/ -lllm 49 | 50 | INCLUDEPATH += $$PWD/../../mnn-llm-android/include 51 | DEPENDPATH += $$PWD/../../mnn-llm-android/include 52 | 53 | ANDROID_EXTRA_LIBS = F:/andriod_env/mnn-llm/project/QwenMNN/../../mnn-llm-android/libs/libMNN.so F:/andriod_env/mnn-llm/project/QwenMNN/../../mnn-llm-android/libs/libMNN_Express.so $$PWD/../../mnn-llm-android/android_build/libllm.so 54 | 55 | 56 | android { 57 | data.files += qwen_model/block_0.mnn 58 | data.files += qwen_model/block_1.mnn 59 | data.files += qwen_model/block_2.mnn 60 | data.files += qwen_model/block_3.mnn 61 | data.files += qwen_model/block_4.mnn 62 | data.files += qwen_model/block_5.mnn 63 | data.files += qwen_model/block_6.mnn 64 | data.files += qwen_model/block_7.mnn 65 | data.files += qwen_model/block_8.mnn 66 | data.files += qwen_model/block_9.mnn 67 | data.files += qwen_model/block_10.mnn 68 | data.files += qwen_model/block_11.mnn 69 | data.files += qwen_model/block_12.mnn 70 | data.files += qwen_model/block_13.mnn 71 | data.files += qwen_model/block_14.mnn 72 | data.files += qwen_model/block_15.mnn 73 | data.files += qwen_model/block_16.mnn 74 | data.files += qwen_model/block_17.mnn 75 | data.files += qwen_model/block_18.mnn 76 | data.files += qwen_model/block_19.mnn 77 | data.files += qwen_model/block_20.mnn 78 | data.files += qwen_model/block_21.mnn 79 | data.files += qwen_model/block_22.mnn 80 | data.files += qwen_model/block_23.mnn 81 | data.files += qwen_model/embedding.mnn 82 | 83 | data.files += qwen_model/embeddings_bf16.bin 84 | data.files += qwen_model/lm.mnn 85 | data.files += qwen_model/tokenizer.txt 86 | 87 | data.path = /assets/models 88 | INSTALLS += data 89 | 90 | } 91 | -------------------------------------------------------------------------------- /QwenMNN.pro.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EnvironmentId 7 | {f00823e8-dbf7-4e5c-a8ca-83ceed64900a} 8 | 9 | 10 | ProjectExplorer.Project.ActiveTarget 11 | 0 12 | 13 | 14 | ProjectExplorer.Project.EditorSettings 15 | 16 | true 17 | false 18 | true 19 | 20 | Cpp 21 | 22 | CppGlobal 23 | 24 | 25 | 26 | QmlJS 27 | 28 | QmlJSGlobal 29 | 30 | 31 | 2 32 | UTF-8 33 | false 34 | 4 35 | false 36 | 80 37 | true 38 | true 39 | 1 40 | true 41 | false 42 | 0 43 | true 44 | true 45 | 0 46 | 8 47 | true 48 | 0 49 | true 50 | true 51 | true 52 | false 53 | 54 | 55 | 56 | ProjectExplorer.Project.PluginSettings 57 | 58 | 59 | -fno-delayed-template-parsing 60 | 61 | true 62 | 63 | 64 | 65 | ProjectExplorer.Project.Target.0 66 | 67 | Android for armeabi-v7a,arm64-v8a,x86,x86_64 (Clang Qt 5.14.2 for Android) 68 | Android for armeabi-v7a,arm64-v8a,x86,x86_64 (Clang Qt 5.14.2 for Android) 69 | {ab37937d-cb74-4041-a493-094052e6badb} 70 | 1 71 | 0 72 | 0 73 | 74 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Android_for_armeabi_v7a_arm64_v8a_x86_x86_64_Clang_Qt_5_14_2_for_Android-Debug 75 | 76 | 77 | true 78 | QtProjectManager.QMakeBuildStep 79 | true 80 | 81 | true 82 | false 83 | false 84 | 85 | 86 | true 87 | Qt4ProjectManager.MakeStep 88 | 89 | false 90 | 91 | 92 | false 93 | 94 | 95 | true 96 | Qt4ProjectManager.AndroidPackageInstallationStep 97 | 98 | 99 | android-28 100 | 101 | true 102 | QmakeProjectManager.AndroidBuildApkStep 103 | false 104 | false 105 | 106 | 4 107 | Build 108 | Build 109 | ProjectExplorer.BuildSteps.Build 110 | 111 | 112 | 113 | true 114 | Qt4ProjectManager.MakeStep 115 | 116 | true 117 | clean 118 | 119 | false 120 | 121 | 1 122 | Clean 123 | Clean 124 | ProjectExplorer.BuildSteps.Clean 125 | 126 | 2 127 | false 128 | 129 | Debug 130 | Qt4ProjectManager.Qt4BuildConfiguration 131 | 2 132 | 133 | 134 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Android_for_armeabi_v7a_arm64_v8a_x86_x86_64_Clang_Qt_5_14_2_for_Android-Release 135 | 136 | 137 | true 138 | QtProjectManager.QMakeBuildStep 139 | false 140 | 141 | false 142 | false 143 | true 144 | 145 | 146 | true 147 | Qt4ProjectManager.MakeStep 148 | 149 | false 150 | 151 | 152 | false 153 | 154 | 155 | true 156 | Qt4ProjectManager.AndroidPackageInstallationStep 157 | 158 | 159 | android-28 160 | 161 | true 162 | QmakeProjectManager.AndroidBuildApkStep 163 | false 164 | false 165 | 166 | 4 167 | Build 168 | Build 169 | ProjectExplorer.BuildSteps.Build 170 | 171 | 172 | 173 | true 174 | Qt4ProjectManager.MakeStep 175 | 176 | true 177 | clean 178 | 179 | false 180 | 181 | 1 182 | Clean 183 | Clean 184 | ProjectExplorer.BuildSteps.Clean 185 | 186 | 2 187 | false 188 | 189 | Release 190 | Qt4ProjectManager.Qt4BuildConfiguration 191 | 0 192 | 193 | 194 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Android_for_armeabi_v7a_arm64_v8a_x86_x86_64_Clang_Qt_5_14_2_for_Android-Profile 195 | 196 | 197 | true 198 | QtProjectManager.QMakeBuildStep 199 | true 200 | 201 | false 202 | true 203 | true 204 | 205 | 206 | true 207 | Qt4ProjectManager.MakeStep 208 | 209 | false 210 | 211 | 212 | false 213 | 214 | 215 | true 216 | Qt4ProjectManager.AndroidPackageInstallationStep 217 | 218 | 219 | android-28 220 | 221 | true 222 | QmakeProjectManager.AndroidBuildApkStep 223 | false 224 | false 225 | 226 | 4 227 | Build 228 | Build 229 | ProjectExplorer.BuildSteps.Build 230 | 231 | 232 | 233 | true 234 | Qt4ProjectManager.MakeStep 235 | 236 | true 237 | clean 238 | 239 | false 240 | 241 | 1 242 | Clean 243 | Clean 244 | ProjectExplorer.BuildSteps.Clean 245 | 246 | 2 247 | false 248 | 249 | Profile 250 | Qt4ProjectManager.Qt4BuildConfiguration 251 | 0 252 | 253 | 3 254 | 255 | 256 | 257 | true 258 | Qt4ProjectManager.AndroidDeployQtStep 259 | false 260 | 261 | 1 262 | Deploy 263 | Deploy 264 | ProjectExplorer.BuildSteps.Deploy 265 | 266 | 1 267 | Qt4ProjectManager.AndroidDeployConfiguration2 268 | 269 | 1 270 | 271 | 272 | dwarf 273 | 274 | cpu-cycles 275 | 276 | 277 | 250 278 | 279 | -e 280 | cpu-cycles 281 | --call-graph 282 | dwarf,4096 283 | -F 284 | 250 285 | 286 | -F 287 | true 288 | 4096 289 | false 290 | false 291 | 1000 292 | 293 | true 294 | 295 | false 296 | false 297 | false 298 | false 299 | true 300 | 0.01 301 | 10 302 | true 303 | kcachegrind 304 | 1 305 | 25 306 | 307 | 1 308 | true 309 | false 310 | true 311 | valgrind 312 | 313 | 0 314 | 1 315 | 2 316 | 3 317 | 4 318 | 5 319 | 6 320 | 7 321 | 8 322 | 9 323 | 10 324 | 11 325 | 12 326 | 13 327 | 14 328 | 329 | 330 | 331 | 332 | 0 333 | 334 | QwenMNN 335 | Qt4ProjectManager.AndroidRunConfiguration:F:/andriod_env/mnn-llm/project/QwenMNN/QwenMNN.pro 336 | F:/andriod_env/mnn-llm/project/QwenMNN/QwenMNN.pro 337 | 338 | false 339 | 340 | false 341 | true 342 | false 343 | false 344 | true 345 | 346 | 1 347 | 348 | 349 | 350 | ProjectExplorer.Project.Target.1 351 | 352 | Desktop Qt 5.14.2 MSVC2017 64bit 353 | Desktop Qt 5.14.2 MSVC2017 64bit 354 | qt.qt5.5142.win64_msvc2017_64_kit 355 | 0 356 | 0 357 | 0 358 | 359 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Desktop_Qt_5_14_2_MSVC2017_64bit-Debug 360 | 361 | 362 | true 363 | QtProjectManager.QMakeBuildStep 364 | true 365 | 366 | false 367 | false 368 | false 369 | 370 | 371 | true 372 | Qt4ProjectManager.MakeStep 373 | 374 | false 375 | 376 | 377 | false 378 | 379 | 2 380 | Build 381 | Build 382 | ProjectExplorer.BuildSteps.Build 383 | 384 | 385 | 386 | true 387 | Qt4ProjectManager.MakeStep 388 | 389 | true 390 | clean 391 | 392 | false 393 | 394 | 1 395 | Clean 396 | Clean 397 | ProjectExplorer.BuildSteps.Clean 398 | 399 | 2 400 | false 401 | 402 | Debug 403 | Qt4ProjectManager.Qt4BuildConfiguration 404 | 2 405 | 406 | 407 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Desktop_Qt_5_14_2_MSVC2017_64bit-Release 408 | 409 | 410 | true 411 | QtProjectManager.QMakeBuildStep 412 | false 413 | 414 | false 415 | false 416 | true 417 | 418 | 419 | true 420 | Qt4ProjectManager.MakeStep 421 | 422 | false 423 | 424 | 425 | false 426 | 427 | 2 428 | Build 429 | Build 430 | ProjectExplorer.BuildSteps.Build 431 | 432 | 433 | 434 | true 435 | Qt4ProjectManager.MakeStep 436 | 437 | true 438 | clean 439 | 440 | false 441 | 442 | 1 443 | Clean 444 | Clean 445 | ProjectExplorer.BuildSteps.Clean 446 | 447 | 2 448 | false 449 | 450 | Release 451 | Qt4ProjectManager.Qt4BuildConfiguration 452 | 0 453 | 454 | 455 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Desktop_Qt_5_14_2_MSVC2017_64bit-Profile 456 | 457 | 458 | true 459 | QtProjectManager.QMakeBuildStep 460 | true 461 | 462 | false 463 | true 464 | true 465 | 466 | 467 | true 468 | Qt4ProjectManager.MakeStep 469 | 470 | false 471 | 472 | 473 | false 474 | 475 | 2 476 | Build 477 | Build 478 | ProjectExplorer.BuildSteps.Build 479 | 480 | 481 | 482 | true 483 | Qt4ProjectManager.MakeStep 484 | 485 | true 486 | clean 487 | 488 | false 489 | 490 | 1 491 | Clean 492 | Clean 493 | ProjectExplorer.BuildSteps.Clean 494 | 495 | 2 496 | false 497 | 498 | Profile 499 | Qt4ProjectManager.Qt4BuildConfiguration 500 | 0 501 | 502 | 3 503 | 504 | 505 | 0 506 | Deploy 507 | Deploy 508 | ProjectExplorer.BuildSteps.Deploy 509 | 510 | 1 511 | ProjectExplorer.DefaultDeployConfiguration 512 | 513 | 1 514 | 515 | 516 | dwarf 517 | 518 | cpu-cycles 519 | 520 | 521 | 250 522 | 523 | -e 524 | cpu-cycles 525 | --call-graph 526 | dwarf,4096 527 | -F 528 | 250 529 | 530 | -F 531 | true 532 | 4096 533 | false 534 | false 535 | 1000 536 | 537 | true 538 | 539 | false 540 | false 541 | false 542 | false 543 | true 544 | 0.01 545 | 10 546 | true 547 | kcachegrind 548 | 1 549 | 25 550 | 551 | 1 552 | true 553 | false 554 | true 555 | valgrind 556 | 557 | 0 558 | 1 559 | 2 560 | 3 561 | 4 562 | 5 563 | 6 564 | 7 565 | 8 566 | 9 567 | 10 568 | 11 569 | 12 570 | 13 571 | 14 572 | 573 | 2 574 | 575 | Qt4ProjectManager.Qt4RunConfiguration:F:/andriod_env/mnn-llm/project/QwenMNN/QwenMNN.pro 576 | F:/andriod_env/mnn-llm/project/QwenMNN/QwenMNN.pro 577 | 578 | false 579 | 580 | false 581 | true 582 | true 583 | false 584 | false 585 | true 586 | 587 | F:/andriod_env/mnn-llm/project/build-QwenMNN-Desktop_Qt_5_14_2_MSVC2017_64bit-Debug 588 | 589 | 1 590 | 591 | 592 | 593 | ProjectExplorer.Project.TargetCount 594 | 2 595 | 596 | 597 | ProjectExplorer.Project.Updater.FileVersion 598 | 22 599 | 600 | 601 | Version 602 | 22 603 | 604 | 605 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## mnn-llm手机端部署大语言模型 2 | 3 | ### 以Qwen1.5-0.5B-chat模型为例详细介绍mnn-llm的使用和在Android端的部署 4 | 5 | DataXujing 6 | 7 | mnn-llm原来只支持特定模型的 ChatGLM-MNN 项目已升级并更名为 mnn-llm,并集成到了MNN项目中;该项目支持多个目前主流的开源LLM模型的部署。与此同时,为了简化不同 LLM 模型向 ONNX 格式导出的流程,mnn-llm团队推出了 llm-export 项目。该项目为多种 LLM 模型提供了统一的导出方案,大大地降低了从预训练模型导出的复杂度。本项目实现了基于mnn-llm的模型转换,windows下的编译和基于QT的android端的编译和调用,并测试了Qwen1.5-0.5B模型在android端的部署。 8 | 9 | ### 1.模型导出 10 | 11 | 在将深度学习模型从研究原型转换为实际可部署的产品时,模型导出阶段的顺畅与否对于整个工作流程至关重要。通常,这个过程涉及将模型从训练框架中导出到一个中间表示,如 ONNX(开放神经网络交换格式),然后再转换为目标部署框架——在本例中为 MNN格式。为了简化并标准化这一过程,mnn-llm团队开发了一个名为 llm-export 的工具。 12 | 13 | llm-export 工具的核心思想在于对大型语言模型(LLM)进行了高度抽象,建立了一个统一化的导出框架。**这个项目的目标是消除将各种 LLM 模型导出到 ONNX 格式的障碍,确保无论是何种架构的 LLM 都能通过一个清晰定义的接口进行处理**。在 llm-export 中,我们定义了一套公用的导出逻辑,这意味着对于任何特定的 LLM,开发者只需实现模型的加载逻辑。这极大地减少了从多样化的训练环境向 ONNX 模型迁移的复杂性,并显著提高了整个导出过程的易用性。**模型一旦被成功导出至 ONNX,即可利用现有的mnnconvert工具转换到 MNN 格式,从而使用MNN完成llm模型的推理**。 14 | 15 | ![](pic/llm-export.png) 16 | 17 | 如上图所示: llm-export中将llm模型抽象为4部分:tokenizer, embedding, blocks, lm 18 | 19 | > 模型导出的Trick:模型拆分优化 20 | 21 | 以ChatGLM-6B为例,首先直接使用torch.onnx.export尝试对模型进行ONNX导出,导出过程非常缓慢,导出后模型的权重大小有28G。在将模型转换到MNN时会执行一些图优化;因为模型太大导致占用内存过高速度非常慢;因此考虑将模型进行拆分优化。拆分之后的优化考虑如下: 22 | 23 | + Embedding层的参数大小为`150528 * 4096`, 单个权重使用内存非常大;考虑到输入文字数量较小(相对于150528),使用Gather实现消耗大量内存/显存,直接将参数存储为二进制文件,通过fseekfread实现Gather的操作能够在稍微牺牲速度的情况下节约2.3G内存; 24 | + GLMBlock层的权重总大小为21G,仍然非常大,每个Block的大小为768M;考虑到要在端侧各类设备部署,可以将28层Block分别导出,这样的好处是能够在显存不足的情况下将部分Block放置在GPU,其余部分放置在CPU进行推理,这样能够充分利用设备算力;同时考虑到移动端设备,如:Android,Raspberry等设备上,在4G以下内存也可以逐Block的加载运行。 25 | + 线性层通过一个矩阵乘将hidden_state转换为词语的prob:`[num, 4096] @ [4096, 150528]`;其实这里并不需要num全部参与运算,比如输入序列长度`num = 10`时,实际预测下一个词语时进需要使用最后一个`[1, 4096]`即可。因此可以先对输入变量做一个Gather然后执行矩阵乘:`[1, 4096] @ [4096, 150528]`即可。同时考虑到MNN中在SIMD架构下的矩阵乘的优化,将改矩阵乘改为`[150528, 4096] @ [4096, 1]`。 26 | 27 | > 模型导出的Trick:Dynamic Shape 28 | 29 | 因为模型输入的形状是动态变化的,因此需要在导出时指定动态形状的维度,但是Pytorch涉及到view操作不支持动态形状,指定了动态维度后,在实际测试中发现因为模型实现中有些view相关代码导出后会将形状固定为常量,导致导出后改变输入形状无法正确推理,因此需要对模型中非动态的实现进行修改,将`attention_fn`函数中所有view操作替换为`squeeze`和`unsqueeze`操作,这样导出后与形状无关即可实现动态形状。 30 | 31 | ```python 32 | # 修改前 33 | query_layer = query_layer.view(output_size[2], output_size[0] * output_size[1], -1) 34 | key_layer = key_layer.view(output_size[3], output_size[0] * output_size[1], -1) 35 | # 修改后 36 | query_layer = query_layer.squeeze(1) 37 | key_layer = key_layer.squeeze(1) 38 | ``` 39 | 40 | > 模型导出的Trick:kv-cache(将Tuple改为Tensor) 41 | 42 | HF中的transformer原始实现中`layer_past`是Tuple类型,将其类型修改为Tensor方便模型导出。将代码中的Tuple操作替换为Tensor操作,如: 43 | 44 | ```python 45 | # 修改前 46 | past_key, past_value = layer_past[0], layer_past[1] 47 | key_layer = torch.cat((past_key, key_layer), dim=0) 48 | value_layer = torch.cat((past_value, value_layer), dim=0) 49 | present = (key_layer, value_layer) 50 | # 修改后 51 | key_layer = torch.cat((past_key_value[0], key_layer), dim=0) 52 | value_layer = torch.cat((past_key_value[1], value_layer), dim=0) 53 | present = torch.stack((key_layer, value_layer), dim=0) 54 | ``` 55 | 56 | 57 | 58 | ### 2.关于mnn和mnn-llm的整体结构 59 | 60 | 在部署大型语言模型(LLM)时,兼容性和易用性是关键因素。为了解决这一挑战,我们开发了一个名为 mnn-llm 的项目。考虑到MNN在跨平台上支持上的优秀表现,该项目基于 MNN 构建,旨在为各种平台提供一个统一的 LLM 模型部署解决方案。mnn-llm 项目使得从 llm-export 导出的模型能够无缝进行端到端的推理,并为开发者提供了一个简易的文本到文本(txt2txt)和多模态的调用接口,目前也只是知识库, 文本嵌入等。 61 | 62 | 在mnn-llm中我们移植实现了目前主流的tokenizer工具:Sentencepiece 和 Tiktoken。这些 tokenizer 组件是处理自然语言输入的关键部分,它们能够将原始文本转换为模型能理解的格式。同时为了轻量化,两种模型都使用文本的方式存储,移除了Sentencepiece中对protobuf的依赖。**此外,考虑到内存占用在移动设备上尤为宝贵,还在 mnn-llm 中引入了 disk embedding 功能**。**这意味着用户可以根据需要选择:在模型推理过程中使用 embedding 模型在内存计算,或者直接从磁盘加载 embedding 值**。这种灵活性不仅降低了运行时的内存需求,也为用户提供了更多的选择来平衡推理性能和资源使用。为了确保 mnn-llm 的通用性和扩展性,我们设计了一种易于扩展的架构。开发者可以通过继承基类并实现特定的配置来支持不同的 LLM 模型。这种架构设计使得整合新的 LLM 模型变得简单快捷,大大降低了将最新的研究成果应用到实际产品中的门槛。 63 | 64 | ![](pic/structure.png) 65 | 66 | ### 3.mnn-lmm的核心优化Trick 67 | 68 | + 基于在ARM-CPU上的耗时分析针对于Linear,MatMul和Memory算子进行优化 69 | 70 | LLM的主干网络由一系列连续的block组成,每个block的核心计算部分是Attention。如图所示,Attention的执行涉及到两个主要的计算操作:Linear(绿色表示)和MatMul(黄色表示)。这两种操作不仅要执行复杂的数学计算,还要伴随诸如split、concat和transpose等内存操作,这些统称为Memory算子。因此,我们可以将LLM模型推理过程中的核心操作分为三类:Linear, MatMul和Memory。 71 | 72 | ![](pic/llm.png) 73 | 74 | 值得注意的是,这三类算子的计算量和内存访问量会受到输入数据的batch大小和kv-cache长度的影响。而根据batch和kv-cahce的输入特点,LLM推理可以被分为两个阶段:prefill(context)和decode(generate)。在prefill阶段,我们输入了一个包含m个token的提示(prompt),执行batch为m的推理,此时由于是初始输入,没有kv-cache的需求,得到了首个token的输出。接着,在decode阶段,我们以上一轮的输出token作为输入,进行batch的推理,这时kv-cache的长度变为m+n,其中n代表之前已生成的token数量。 75 | 76 | 如上图左的图表所示,通过在ARM-CPU上的测试,可以详细分析在这两个阶段中上述三类核心算子的耗时情况。例如,单个block的耗时分析显示: 77 | 78 | 1. 在prefill阶段,Linear算子的耗时占比相对稳定,超过了93%,而MatMul和Memory算子的耗时占比分别约为3%和2%; 79 | 2. 在decode阶段,随着m+n的增长,Linear算子的时间占比有所下降,而MatMul和Memory算子的占比有所上升。尽管如此,在多数情况下,耗时主要集中在Linear算子上。 80 | 81 | 综合以上分析,mnn-llm将优化工作的重点放在Linear算子上。通过对Linear计算的深入优化,mnn-llm实现整个LLM推理过程的性能提升。这将是提高LLM整体推理效率的关键所在,尤其是在资源受限的设备上,每一次性能的增益都至关重要。 82 | 83 | 在LLM模型推理过程中,线性层(Linear layers)的计算效率对于整体性能至关重要。这个计算主要分为两个阶段:prefill阶段处理大量输入数据时,线性层是矩阵乘法(GEMM),这是一个计算密集的过程;在模型解码(decode)阶段时,线性层的计算是矩阵向量乘法(GEMV),这时候访存的效率变得更加关键。为了优化这两个阶段的性能,mnn-llm采取了不同的技术策略:计算密集型的优化和访存密集型的优化 84 | 85 | + 计算密集型的优化 86 | 87 | 对于计算密集型的优化:mnn-llm关注于使用更强大的计算指令,选择计算峰值更高的**SIMD指令集**来完成核心的计算,同时使用汇编实现多种规模的kernel,以此来加速矩阵乘法操作。 88 | 89 | + 访存密集型的优化 90 | 91 | 对于访存密集型的优化:mnn-llm的策略是通过降低数据的位宽来减少内存访问量(**量化技术**)和**数据重排**来实现。选择了 W4A8 的量化方案,即将模型中的权重(W)量化到 4 位,而将激活值(A,即模型的输入和输出)量化到 8 位。这样做可以大幅减少模型的内存占用,并提升性能,因为较低位宽的数据需要更少的内存带宽来读写;**同时针对W4A8的量化方案按照设备支持的最优SIMD计算指令对数据进行特定的重排以提升内存局部性,提升访存效率**。 92 | 93 | > mnn-llm采用了采取了针对输入的动态量化方案,即W4A8的量化方案 94 | 95 | 在当今流行的大型语言模型(LLM)中,线性层的权重数量达到了惊人的规模,常常包含数十亿个参数。举个例子,**一个70亿参数(7b)的模型,即使采用16位浮点(fp16)来存储,也需要约14GB的内存空间。这种规模的模型在内存受限的移动设备上部署是一大挑战。**为了解决这个问题,mnn-llm必须采取措施来压缩这些权重,使它们占用更少的内存。量化技术为提供了一条可行之路。幸运的是,LLM中的线性层权重之间的差异相对较小,这使得它们非常适合进行低比特量化——即使用较少的比特表示每个权重值。即使经过量化,计算结果仍能保持与原始浮点计算高度一致,这表明量化对模型性能的影响很小。**考虑到移动端设备的内存大小,mnn-llm选择了4位量化作为解决方案。通过这种方法,那个需要14G内存的7b模型现在只需大约3.5GB内存即可运行。**这意味着拥有8GB内存的设备也能够运行这样一个大模型。mnn-llm采用了非对称量化方案,具体的4位量化公式是: 96 | 97 | $$qx=round\frac{15(x-x_{min})}{(x_{max}-x_{min})})$$ 98 | 99 | 采用这种量化方法的一个巨大优势在于,计算过程中模型的权重访存量可以减少四倍,从而有效提高访存性能。 100 | 101 | 在量化权重的同时,mnn-llm也审视了模型输入的处理方式。**过去,利用混合精度计算,即结合了4位量化的权重与16位或32位浮点的输入。这要求在计算前将量化后的权重解量化为浮点数,同时从内存中加载浮点型的输入数据,接着进行浮点乘加运算。这种方法在处理输入时需要较多的内存访问,并且由于权重是以行优先的方式存储的,这进一步增加了内存访问量。**另外,由于涉及浮点计算,它使用浮点型的SIMD指令,其峰值性能低于整数型指令。 102 | 103 | **为了应对这些问题,mnn-llm采取了针对输入的动态量化方案,即W4A8的量化方案。**首先需要在运行时统计输入的分布情况,使用`absmax-quant`的方案将输入量化为8bit整形作为线性性层的输入。然后将4bit量化的权重加载并转换为8位整数值,使用8位整数来完成矩阵乘的乘累加操作,使用32位整数来保存累加结果。最终,在整个计算过程结束后,会将这些累加结果反量化回浮点数。采用W4A8技术不仅显著降低了内存访问量,还使mnn-llm能够利用更高算力的整数计算指令,大幅提升了模型的计算性能。 104 | 105 | ![](pic/quant.png) 106 | 107 | + Disk Embedding 108 | 109 | 考虑到内存占用在移动设备上尤为宝贵, mnn-llm 中引入了 disk embedding 功能**。**这意味着用户可以根据需要选择:在模型推理过程中使用 embedding 模型在内存计算,或者直接从磁盘加载 embedding 值 110 | 111 | + 词表瘦身 112 | 113 | 以ChatGLM-6B为例,词表大小为150528, 分析词表内容可以发现前20000个词语为一些特殊字符,在Chat中并没有使用,因此可以将结构拆分后的Embedding层和最后的线性层进行删减。简单的方法是将前2层的权重导出onnx模型,使用numpy.fromfile将onnx模型的权重加载,删除前`[20000, 4096]`的部分,在使用numpy.tofile保存即可。代码如下: 114 | 115 | ```python 116 | import numpy as np 117 | embed = np.fromfile('transformer.word_embeddings.weight', dtype=np.float32, count=-1, offset=0) 118 | embed = embed.reshape(-1, 4096) # shape is (150528, 4096) 119 | embed = embed[20000:, :] # shape is (130528, 4096) 120 | embed.tofile('slim_embedding.bin') 121 | 122 | ``` 123 | 124 | + MNN低内存模式增强 125 | 126 | **MNN推理时支持对推理时的精度,内存与功耗等进行配置**;打开低精度会在支持的设备上使用fp16进行推理;打开低内存模式则会取消一些占用内存的性能优化,如不会使用[Winograd](https://zhenhuaw.me/blog/2019/gemm-optimization.html)优化卷积;**mnn-llm新增了权值量化的模型的低内存推理**,可以通过内存选项来配置是否使用计算时量化来控制内存占用。对于没有内存瓶颈的中小模型可以使用正常内存模式,即加载阶段反量化来提升推理速度;对于具有内存瓶颈的大模型,则使用低内存模式,计算时反量化来降低内存占用与访存带宽,提升速度。 127 | 128 | 对于weight量化模型的低内存实现,mnn-llm支持了int4和int8两种权值量化的模型的低内存模式。针对不同的硬件做了实现,针对X86 SSE, AVX2实现了int4@fp32, int8@fp32;针对ARM64实现了int4@fp32, int8@fp32和int4@fp16和int8@fp16。具体的是线上需要针对以上列举的情况分别实现对应的矩阵乘Kernel,并且在原来的浮点矩阵乘的输入里增加反量化需要的alpha和bias参数,在矩阵乘计算前需要先从内存中加载常量的int4/int8量化值,然后将其转换为浮点类型,之后再执行浮点矩阵乘操作,实际的矩阵乘基础操作如下公式: 129 | 130 | $$\sum^{m,n}_{i,j}y_{i,j}+=x_{i,k}*(\alpha * w_{k,j}+bias_{j})$$ 131 | 132 | ### 4.大模型在mnn-llm中的推理过程 133 | 134 | 我们以ChatGLM-6B为例说明mnn-llm中的推理过程: 135 | 136 | **推理过程:** 137 | 138 | 用户输入n个词语,在前处理转换为n个int后,通过对embedding数据的查询会生成一个`[n, 4096]`的向量,同时根据输入长度和结束符位置,生成`position_ids`和`mask`作为输入;对于非首次生成还会有一个历史信息的输入。其中`position_ids`和`mask`对于28个block完全一样,history每个block都使用上次生成时对应block的当前输出;而输入的input_embedding则使用上一个block的输出hidden_state,具体结构如下: 139 | 140 | ![](pic/inference.png) 141 | 142 | **前处理:** 143 | 144 | 官方提供的实现中使用了transformers库,该库提供了模型的前后处理的实现。其中前处理包括了分词,将词语转换为ids;后处理中包含了prob转换为词语,控制模型持续生成的逻辑。在转换到C++之后我们也需要实现相同的前后处理逻辑。 145 | 146 | 前处理逻辑是将用户输入的句子进行分词,然后查询词表将词语转换为id;C++中实现如下: 147 | 148 | - 分词: 在C++上使用cppjieba进行分词,现在支持Sentencepiece和TikToken的分词实现; 149 | - word2id: 将词表文件加载为map,通过查询map将词语转换为id; 150 | 151 | **后处理:** 152 | 153 | 后处理部分是将prob转换为id,然后通过词表将id转换为词语,同时将一些特殊字符进行转义;C++中实现如下: 154 | 155 | - prob2id:在lm层后接一个ArgMax即可将prob转换为id,实测效果与transformers中的实现结果一致; 156 | - id2word: 词表文件加载为vector,直接读取即可获取word; 157 | - 特殊词处理:针对一些特殊词语进行了替换; 158 | 159 | ### 5.编译Windows版本mnn-llm 160 | 161 | + clone mnn-llm项目 162 | 163 | ``` 164 | git clone https://github.com/wangzhaode/mnn-llm 165 | ``` 166 | 167 | + 使用VS2017编译器进行编译 168 | 169 | ```shell 170 | cd mnn-llm 171 | 172 | # 1. clone MNN 173 | git clone https://github.com/alibaba/MNN.git --depth=1 174 | 175 | # 2. build MNN 176 | cd MNN 177 | mkdir build 178 | cd build 179 | cmake -DMNN_LOW_MEMORY=ON -DMNN_WIN_RUNTIME_MT=ON .. 180 | cmake --build . --config Release -j 4 181 | cd ../.. 182 | 183 | # 3. copy headers and libs 184 | cp -r MNN\include\MNN include 185 | cp MNN\build\Release\MNN.lib libs 186 | cp MNN\build\Release\MNN.dll libs 187 | 188 | # 4. copy pthread 189 | Expand-Archive .\resource\win_pthreads.zip 190 | cp .\win_pthreads\Pre-built.2\lib\x64\pthreadVC2.lib libs 191 | cp .\win_pthreads\Pre-built.2\include\*.h .\include\ 192 | 193 | # 5. build mnn-llm 194 | mkdir build 195 | cd build 196 | cmake .. 197 | cmake --build . --config Release -j 4 198 | cd .. 199 | ``` 200 | 201 | ### 6.编译android版本mnn-llm 202 | 203 | 测试在Ubuntu16.04LTS下进行: 204 | 205 | ```shell 206 | # 1.NDK r21配置 207 | # 下载Linux版 NDK-r21, 并解压 208 | # 设置NDK环境变量和访问权限 209 | export ANDROID_NDK=/home/myuser/xujing/test/android-ndk-r21e 210 | chmod -R 777 /home/myuser/xujing/test/android-ndk-r21e 211 | 212 | # 2.按照如下步骤编译android版mnn-llm 213 | 214 | # 1. clone MNN 215 | git clone https://github.com/alibaba/MNN.git --depth=1 216 | 217 | # 2. build MNN 218 | cd MNN/project/android 219 | mkdir build 220 | cd build 221 | ../build_64.sh -DMNN_LOW_MEMORY=ON 222 | cd ../../../.. 223 | 224 | # 3. copy headers and libs 225 | cp -r MNN/include/MNN include 226 | cp MNN/project/android/build/libMNN.so MNN/project/android/build/libMNN_Express.so libs 227 | 228 | # 4. build mnn-llm android 229 | mkdir android_build 230 | cd android_build 231 | cmake .. \ 232 | -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ 233 | -DANDROID_STL=c++_static \ 234 | -DANDROID_ABI="arm64-v8a" \ 235 | -DANDROID_NATIVE_API_LEVEL=android-21 \ 236 | -DCMAKE_BUILD_TYPE=Release \ 237 | -DBUILD_FOR_ANDROID=ON 238 | make -j4 239 | cd .. 240 | ``` 241 | 242 | ### 7.mnn-llm windows下调试测试 243 | 244 | 项目在`./windows/Qwen0.5B.cpp`下,其调用方式如下: 245 | 246 | ```C++ 247 | #include "llm.hpp" 248 | #include 249 | #include 250 | #include 251 | #include 252 | #include 253 | 254 | using namespace std; 255 | 256 | int main() { 257 | 258 | std::string model_dir ="./qwen_model"; 259 | 260 | std::cout << "model path is " << model_dir << std::endl; 261 | std::unique_ptr llm(Llm::createLLM(model_dir,"qwen1.5-0.5b-chat")); 262 | 263 | llm->load(model_dir); 264 | //llm->chat(); 265 | //llm->warmup(); 266 | std::string prompt = "写一首关于清明的诗"; 267 | 268 | string output; 269 | std::cout << prompt << std::endl; 270 | output = llm->response(prompt); 271 | std::cout << "--------------------" << std::endl; 272 | 273 | std::string prompt1 = "马云是谁"; 274 | std::cout << prompt1 << std::endl; 275 | output = llm->response(prompt1); 276 | 277 | return 0; 278 | } 279 | 280 | ``` 281 | 282 | ### 8.基于QT和mnn-llm的安卓程序测试 283 | 284 | 我们通过QT实现了一个简陋的Android程序,目的是测试mnn-llm的大模型在android下的运行状况,核心代码在`widget.cpp`中,最后我们成功在android手机上部署了Qwen1.5-0.5B模型。 285 | 286 | Reference: 287 | 288 | https://github.com/wangzhaode/mnn-llm 289 | 290 | https://zhuanlan.zhihu.com/p/673006723 291 | 292 | https://blog.csdn.net/qq_45038038/article/details/135668575 293 | 294 | https://zhuanlan.zhihu.com/p/626428648 295 | 296 | 297 | https://zhuanlan.zhihu.com/p/639382117 298 | 299 | -------------------------------------------------------------------------------- /android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.0' 9 | } 10 | } 11 | 12 | repositories { 13 | google() 14 | jcenter() 15 | } 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | dependencies { 20 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 21 | } 22 | 23 | android { 24 | /******************************************************* 25 | * The following variables: 26 | * - androidBuildToolsVersion, 27 | * - androidCompileSdkVersion 28 | * - qt5AndroidDir - holds the path to qt android files 29 | * needed to build any Qt application 30 | * on Android. 31 | * 32 | * are defined in gradle.properties file. This file is 33 | * updated by QtCreator and androiddeployqt tools. 34 | * Changing them manually might break the compilation! 35 | *******************************************************/ 36 | 37 | compileSdkVersion androidCompileSdkVersion.toInteger() 38 | 39 | buildToolsVersion '28.0.3' 40 | 41 | sourceSets { 42 | main { 43 | manifest.srcFile 'AndroidManifest.xml' 44 | java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] 45 | aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] 46 | res.srcDirs = [qt5AndroidDir + '/res', 'res'] 47 | resources.srcDirs = ['resources'] 48 | renderscript.srcDirs = ['src'] 49 | assets.srcDirs = ['assets'] 50 | jniLibs.srcDirs = ['libs'] 51 | } 52 | } 53 | 54 | lintOptions { 55 | abortOnError false 56 | } 57 | 58 | // Do not compress Qt binary resources file 59 | aaptOptions { 60 | noCompress 'rcc' 61 | } 62 | 63 | defaultConfig { 64 | resConfigs "en" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/android/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /android/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/android/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /android/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/android/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /android/res/values/libs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://download.qt.io/ministro/android/qt5/qt-5.14 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "widget.h" 2 | 3 | #include 4 | //#include 5 | #include "llm.hpp" 6 | #include 7 | #include 8 | #include 9 | 10 | std::unique_ptr llm(nullptr); 11 | 12 | 13 | bool copyDirectoryFiles(const QString &fromDir, const QString &toDir, bool coverFileIfExist) 14 | { 15 | QDir sourceDir(fromDir); 16 | QDir targetDir(toDir); 17 | if(!targetDir.exists()){ /**< 如果目标目录不存在,则进行创建 */ 18 | if(!targetDir.mkdir(targetDir.absolutePath())) 19 | return false; 20 | } 21 | 22 | QFileInfoList fileInfoList = sourceDir.entryInfoList(); 23 | foreach(QFileInfo fileInfo, fileInfoList){ 24 | if(fileInfo.fileName() == "." || fileInfo.fileName() == "..") 25 | continue; 26 | 27 | if(fileInfo.isDir()){ /**< 当为目录时,递归的进行copy */ 28 | if(!copyDirectoryFiles(fileInfo.filePath(), 29 | targetDir.filePath(fileInfo.fileName()), 30 | coverFileIfExist)) 31 | return false; 32 | } 33 | else{ /**< 当允许覆盖操作时,将旧文件进行删除操作 */ 34 | if(coverFileIfExist && targetDir.exists(fileInfo.fileName())){ 35 | targetDir.remove(fileInfo.fileName()); 36 | } 37 | 38 | /// 进行文件copy 39 | if(!QFile::copy(fileInfo.filePath(), 40 | targetDir.filePath(fileInfo.fileName()))){ 41 | return false; 42 | } 43 | } 44 | } 45 | return true; 46 | } 47 | 48 | 49 | int main(int argc, char *argv[]) 50 | { 51 | // qputenv("QT_IM_MODULE",QByteArray("qtvirtualkeyboard")); 52 | QApplication a(argc, argv); 53 | QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 54 | 55 | //模型加载放在main函数 56 | // std::string model_dir ="assets:/models"; 57 | copyDirectoryFiles("assets:/models","models", false); 58 | if (!llm.get()) { 59 | llm.reset(Llm::createLLM("models","qwen1.5-0.5b-chat")); 60 | llm->load("models"); 61 | qDebug() << "---------------------------------------->>>>: Qwen1.5-0.5b模型加载完成"; 62 | 63 | 64 | } 65 | 66 | Widget w; 67 | // QVBoxLayout layout; 68 | // w.setLayout(&layout); 69 | // w.showFullScreen(); 70 | w.show(); 71 | return a.exec(); 72 | } 73 | -------------------------------------------------------------------------------- /pic/inference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/pic/inference.png -------------------------------------------------------------------------------- /pic/llm-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/pic/llm-export.png -------------------------------------------------------------------------------- /pic/llm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/pic/llm.png -------------------------------------------------------------------------------- /pic/quant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/pic/quant.png -------------------------------------------------------------------------------- /pic/qwen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/pic/qwen.png -------------------------------------------------------------------------------- /pic/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataXujing/Qwen1.5-0.5b-chat-android/2ddcc34390a2a82f249d1a5a6fceb4661c88547c/pic/structure.png -------------------------------------------------------------------------------- /widget.cpp: -------------------------------------------------------------------------------- 1 | #include "widget.h" 2 | #include "ui_widget.h" 3 | #include 4 | //#include 5 | #include "llm.hpp" 6 | #include 7 | #include 8 | #include 9 | 10 | extern std::unique_ptr llm; 11 | static std::stringstream response_buffer; 12 | 13 | Widget::Widget(QWidget *parent) 14 | : QWidget(parent) 15 | , ui(new Ui::Widget) 16 | { 17 | ui->setupUi(this); 18 | ui->textEdit->setReadOnly(true); 19 | 20 | 21 | } 22 | 23 | Widget::~Widget() 24 | { 25 | delete ui; 26 | } 27 | 28 | 29 | void Widget::on_pushButton_clicked() 30 | { 31 | 32 | 33 | qDebug() << "这是button 被click" ; 34 | QString prompt = ui->textEdit_2->toPlainText(); 35 | ui->textEdit_2->clear(); 36 | // QTextDocumentFragment fragment; 37 | // fragment = QTextDocumentFragment::fromHtml(""); 38 | // ui->textEdit->textCursor().insertFragment(fragment); 39 | // ui->textEdit->setAlignment(Qt::AlignLeft); 40 | ui->textEdit->insertPlainText("Prompt: "+prompt +"\n"); 41 | //调用qwen大模型回复 todo 42 | 43 | //llm->chat(); 44 | //llm->warmup(); 45 | std::string output_str; 46 | std::string prompt_str = prompt.toStdString(); 47 | // output_str = llm->response(prompt_str); 48 | // llm->response(prompt_str, &response_buffer, ""); 49 | // QString output = QString::fromStdString(output_str); 50 | 51 | // ui->textEdit->setAlignment(Qt::AlignRight); 52 | // ui->textEdit->insertPlainText( "Qwen: "+ output +"\n"); 53 | QString output = "sucess"; 54 | qDebug() << "--------------->>> 准备调用模型!"; 55 | // output_str = llm->response(prompt_str, &response_buffer); 56 | output_str = llm->response(prompt_str); 57 | // output_str = response_buffer.str(); 58 | output = QString::fromStdString(output_str); 59 | 60 | // if (llm.get() && llm->load_progress() >= 100) { 61 | 62 | 63 | // //新的调用方式 64 | // const char* input_str = prompt_str.data(); 65 | // auto chat = [&](std::string str) { 66 | // llm->response(str, &response_buffer); 67 | // output_str = response_buffer.str(); 68 | // output = QString::fromStdString(output_str); 69 | // }; 70 | // std::thread chat_thread(chat, input_str); 71 | // chat_thread.detach(); 72 | 73 | // } 74 | 75 | // ui->textEdit->setAlignment(Qt::AlignRight); 76 | ui->textEdit->insertPlainText( "Qwen: "+ output +"\n\n"); 77 | //清空stringstream 78 | // response_buffer.str(""); 79 | llm->reset(); // 清空history,暂时不要多伦对话,真实可以固定多伦对话关联的轮次 80 | 81 | } 82 | 83 | void Widget::on_textEdit_2_selectionChanged() 84 | { 85 | // ui->textEdit_2->setInputMethodHints(Qt::ImhExclusiveInputMask); 86 | // QInputMethod *keyboard = QGuiApplication::inputMethod(); 87 | // keyboard->show(); 88 | 89 | } 90 | -------------------------------------------------------------------------------- /widget.h: -------------------------------------------------------------------------------- 1 | #ifndef WIDGET_H 2 | #define WIDGET_H 3 | 4 | #include 5 | 6 | QT_BEGIN_NAMESPACE 7 | namespace Ui { class Widget; } 8 | QT_END_NAMESPACE 9 | 10 | class Widget : public QWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | Widget(QWidget *parent = nullptr); 16 | ~Widget(); 17 | 18 | private slots: 19 | void on_pushButton_clicked(); 20 | 21 | void on_textEdit_2_selectionChanged(); 22 | 23 | private: 24 | Ui::Widget *ui; 25 | }; 26 | #endif // WIDGET_H 27 | -------------------------------------------------------------------------------- /widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Widget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 775 10 | 865 11 | 12 | 13 | 14 | 15 | 775 16 | 0 17 | 18 | 19 | 20 | 21 | 775 22 | 865 23 | 24 | 25 | 26 | Widget 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 16777215 36 | 150 37 | 38 | 39 | 40 | font: 14pt "微软雅黑"; 41 | 42 | 43 | 44 | 45 | 46 | 47 | font: 16pt "幼圆"; 48 | 49 | 50 | 51 | 52 | 53 | 54 | font: 16pt "幼圆"; 55 | 56 | 57 | 发送 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /windows/Qwen0.5B.cpp: -------------------------------------------------------------------------------- 1 | // Qwen0.5B.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 2 | // 3 | 4 | #include "llm.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace std; 12 | 13 | int main() { 14 | 15 | std::string model_dir ="./qwen_model"; 16 | 17 | std::cout << "model path is " << model_dir << std::endl; 18 | std::unique_ptr llm(Llm::createLLM(model_dir,"qwen1.5-0.5b-chat")); 19 | 20 | llm->load(model_dir); 21 | //llm->chat(); 22 | //llm->warmup(); 23 | std::string prompt = "写一首关于清明的诗"; 24 | 25 | string output; 26 | std::cout << prompt << std::endl; 27 | output = llm->response(prompt); 28 | std::cout << "--------------------" << std::endl; 29 | 30 | std::string prompt1 = "马云是谁"; 31 | std::cout << prompt1 << std::endl; 32 | output = llm->response(prompt1); 33 | 34 | return 0; 35 | } 36 | --------------------------------------------------------------------------------