• 首页
  • 加入
  • RSS
  • Thursday, November 30, 2023

    2023 年 11 月 21 日,Qt Group 在上海成功举办了“Qt 全球峰会 2023 中国站”,本次大会上发布了关于 Qt 开放框架与开发工具的战略,集中展示了最新的产品、解决方案,阐述了它们在优化软件跨平台开发全流程、提升研发团队跨职能协作效率以及加速产品迭代速度等方面的强大优势,同时大会上也分享了对于人工智能如何影响软件开发和测试的思考。

    pic1

    Qt 作为 Linux 上最重要的开发工具之一,为中国信创产业的发展和创新提供了更多的可能性和选择。Qt Group 中国区负责人许晟在开幕致辞中表示,“Qt 中国作为 Qt Group 在中国的本地化团队,不仅通过源代码交付帮助本地企业实现软件的自主创新,还基于强大的跨平台能力支持针对本地操作系统和主流芯片的开发,为中国信创产业的发展和创新提供了更多的可能性和选择。

    pic2

    值得一提的是,deepin 已为 Qt社区贡献 100000+ 行代码,在贡献者中名列前茅。此外,deepin 社区也基于 Qt 开发了一整套简单且实用的通用开发框架 Development ToolKit(简称DTK),DTK 处于deepin系统中的核心位置。DDE 中超过30+组件,如浏览器、音乐、邮件等40余款原生应用全部使用 DTK 开发。为此,deepin(深度)社区在 deepin 开发者平台上专门提供了在 deepin 上进行 Qt 开发的文档指导。

    2023 年 11 月 18 日,deepin(深度)社区在北京798艺术中心举办了第十三届深度开发者与用户大会(Deepin Developer&User Conference,简称DDUC),在本次大会上,deepin社区也是公开了近一年在 DTK 和 Qt 这块的建设成果,目前 DTK 已正式适配 Qt6(6.4.2),实现全面升级,相关介绍可以查看《deepin(深度)宣布 deepin DTK 已完成基于 Qt6 的全面升级!》

    pic3

    在本次 QtWS23 峰会上,Qt Group 资深开发工程师齐亮介绍了Qt Wayland 的最新进展。近些年 Linux 桌面发行版迁移到 Wayland 的趋势愈发明显,大有替代 X11 之势。Ubuntu 目前已经默认 Wayland 会话,GNOME 在 Wayland 支持这块一直走在 Linux 发行版的前列,且动作非常激进,目前 GNOME 桌面已经对外宣布将移除对 X.Org 会话支持,默认使用 Wayland。X11 诞生于 1984 年,其设计符合当时的硬件环境、用户需求。随着技术的发展,出现了很多问题,特别是在桌面环境里面,需要 Xorg、窗口管理器、桌面环境组件三者之间进行复杂的交互、协作,导致演进困难。同时 X11 对于新特性的支持较差,Hi-DPI 体验糟糕,并且完全不支持 HDR。

    事实上,在 2023 年5 月 17 日,deepin(深度)社区在推出 deepin V23 Beta 版的同时也已经支持了 Wayland 桌面环境。在 V23 beta 版本中,DDE 试验性的开启了 Wayland 的支持,允许用户在 Wayland 协议下的桌面工作环境启动 。在 deepin V23 中支持 Wayland,是 DDE 的一个非常重要的特性,也是今后 deepin 团队的工作重点,以实现 X11 版本的完全替代,提升 DDE 的优质体验,在 Wayland 桌面环境领域达到领先水平。

    DDE(deepin desktop environment)是deepin(深度)社区自主开发的美观易用、极简操作的桌面环境,主要由桌面、启动器、任务栏、控制中心、窗口管理器等组成,系统中预装了深度特色应用,它既能让您体验到丰富多彩的娱乐生活,也可以满足您的日常工作需要。

    在此背景下,deepin 团队在今年 DDUC 大会了宣布将推出 Treeland 作为今后 DDE 所有功能开发的核心。Treeland 的底层基于 wlroots,并与 Qt Quick 进行了绑定,可同时兼顾两者的优点,wlroots 是 Wayland 生态中发展最迅速的开发库之一,具有功能丰富、演进速度快等优势,将其与 Qt Quick 结合则可以弥补 wlroots 在 GUI 能力方面的欠缺,极大的降低 Wayland 合成器的开发难度,实现 Vulkan、OpenGL ES2、软件渲染等多种渲染方式的无缝切换。

    pic4

    DDE 的新架构,将桌面环境各技术领域的组件进行了统一设计,允许桌面环境开发人员对其进行完全掌控,可轻松实现设备共享、多端无缝协同等高级功能。

    pic5

    未来,deepin(深度)社区将继续与最新技术保持同步,持续推进 DTK 的改进优化,也期待更多感兴趣的朋友加入到 deepin 开源社区中来,讨论更多内容,为推动生态发展贡献力量。

    Wednesday, November 8, 2023

    本文简述了deepin v23系统使用5G WWAN网卡连接互联网的方式,仅仅作为一个临时解决方案和技术验证使用,后期会在系统中内置此功能。

    第一步:安装modemmanager

    modemmanager是一个由freedesktop托管的项目,旨在在linux设备上运行调制解调器以让linux设备获得蜂窝无线网络连接的能力,所以我们第一步骤就是安装此软件。目前deepin已经支持此软件最新版本:

    sudo apt install modemmanager
    

    同时,你需要确保你的内核模块已经正确加载了你的WWAN驱动,你可以通过使用lspci -vvv查看详细信息

    在安装了modemmanager后,你可以通过mmcli 命令使用命令行与其交互,使用mmcli —help-all来获取全部的帮助选项。

    第二步:使用mmcli连接5G网络

    使用mmcli -L 获取你的wwan卡信息,返回的信息有三个内容,分别为:DBus地址、设备类型 、设备ID,其中DBus地址的最后一位为设备编号,你可以使用mmcli --modem=<设备编号>的方式查看设备详细支持信息。

    如果你的SIM卡设置了PIN锁,则需要在连接网络之前使用mmcli --modem=0 --sim=0 --pin=**** 的方式连接,而后我们就可以启动相关设备了:

    mmcli --modem=<设备编号> --enable
    

    然后你需要使用simple connect连接网络

    mmcli -m <设备编号> --simple-connect='apn=<apn名>,ip-type=ipv4v6'
    

    比如我的连接方式为

    mmcli -m 4 --simple-connect='apn=ctnet,ip-type=ipv4v6'
    

    此时你再使用mmcli -m <设备编号> 查看信息的时候 可以查看Bearer的信息,Bearer的DBus的最后一位为其编号。使用命令查看Bearer的信息:

    mmcli -m <设备编号> -b <Bearer编号>
    

    如果你看到Bearer相关信息,就几乎接近成功了:

      ------------------------------------
      General            |           path: /org/freedesktop/ModemManager1/Bearer/0
                         |           type: default
      ------------------------------------
      Status             |      connected: yes
                         |      suspended: no
                         |    multiplexed: no
                         |      interface: wwan0
                         |     ip timeout: 20
      ------------------------------------
      Properties         |            apn: ctnet
                         |        roaming: allowed
                         |        ip type: ipv4
                         |   allowed-auth: none, pap, chap, mschap, mschapv2, eap
                         |           user: [email protected]
                         |       password: vnet.mobi
      ------------------------------------
      IPv4 configuration |         method: static
                         |        address: 10.122.58.19
                         |         prefix: 8
                         |        gateway: 10.122.58.17
                         |            dns: 202.103.24.68, 202.103.44.150
                         |            mtu: 1420
      ------------------------------------
      Statistics         |     start date: 2023-11-07T05:40:08Z
                         |       duration: 1260
                         |   uplink-speed: 1250000000
                         | downlink-speed: 4670000000
                         |       attempts: 1
                         | total-duration: 1260
    
    

    第三步:使用nmcli打开连接

    networkmanager对mm是有做支持,在你完成上述步骤之后,可以通过ip a命令查看,可以看到一个被down掉的接口,我们使用nmcli查看其详细信息:

    nmcli device show
    

    你就可以 看到一个以wwan开头的设备:

    GENERAL.DEVICE:                         wwan0mbim0
    GENERAL.TYPE:                           gsm
    GENERAL.HWADDR:                         (未知)
    GENERAL.MTU:                            1420
    GENERAL.STATE:                          100(已连接)
    GENERAL.CONNECTION:                     wwan0mbim0
    GENERAL.CON-PATH:                       /org/freedesktop/NetworkManager/ActiveConnection/3
    IP4.ADDRESS[1]:                         10.122.58.19/8
    IP4.GATEWAY:                            10.122.58.17
    IP4.ROUTE[1]:                           dst = 10.0.0.0/8, nh = 0.0.0.0, mt = 700
    IP4.ROUTE[2]:                           dst = 0.0.0.0/0, nh = 10.122.58.17, mt = 700
    IP4.DNS[1]:                             202.103.24.68
    IP4.DNS[2]:                             202.103.44.150
    IP6.GATEWAY:                            --
    
    

    使用下述命令打开此设备

    nmcli d connect <设备名>
    

    上述设备名就是以wwan开头的设备

    然后你就可以使用5G网络连接了

    Saturday, November 4, 2023

    介绍

    xtrace是一个用于跟踪分析X11图形协议通信的工具,它可以监控和记录X11服务器上的各种场景,以帮助开发人员诊断和调试与图形界面相关的问题。作为一款强大的工具,xtrace可用于逆向工程、调试分析、性能分析等领域。在Linux X11系统中,xtrace能够记录一个程序在运行时所发起的X11协议请求和XServer发送给程序的事件,以及这些调用的参数。这对于在不阅读源码情况排查程序中的问题、理解程序行为、分析性能瓶颈以及进行协议审计都非常有用。笔者在工作过程中使用该工具深度剖析过腾讯会议、simplescreenrecorder等应用程序的实现,在没有阅读代码的前提下可以获得软件录屏的工作流程,配合阅读常规的X11录屏代码,可分析出其部分工功能异常原因,这些经验在xwayland适配X11应用程序截图录屏项目中通过实战解决了一系列问题。

    本文主要介绍的是X11协议的监视工具,希望您在读完本篇文章后可以对如何监控X11协议有比较深刻的认知,在工作中经常会碰到一些X11应用程序运行时功能异常,在没有应用程序源码的情况下通过xtrace进行调试是一个不错的选择,希望在阅读完这篇文章后,能丰富您的调试技巧。祝您阅读愉快!

    xtrace的安装和使用非常简单,打开终端像如下输入命令即可启动工具:

    // UOS上安装xtrace,deepin上没有可以在http://snapshot.debian.org/上下载
    sudo apt install xtrace
    
    // 运行之后协议电报内容会存储在/tmp/dde-calendar.xtrace中
    xtrace -n -o /tmp/dde-calendar.xtrace dde-calendar	
    
    命令行选项含义或用途
    –display, -d用于ssh远程调试,例如:xtrace -d :0 dde-calendar
    –outfile, -o用于将通信内容转存到磁盘,例如:xtrace -o /tmp/x.log dde-calendar
    –stopwhendone, -s进程退出后停止xtrace,例如:xtrace -s dde-calendar

    工作原理

    如图1所示,X11其主要有两种通信方式,在同一个计算机内主要使用unix domain socket进行通信;在不同计算机之间使用TCP/IP通信。只需要“截获”通信消息,将其转为易于读取的格式输出即可达到监控目的。这是xtrace工作的基本原理。

    图1. 不同客户端使用同一个XServer显示器框架图

    如下图2是X11窗口创建的基本流程,因为篇幅限制,图中只绘制了基础的请求和事件,但X11协议远比图中绘制的复杂,笔者在此只介绍一下基础的协议交互模型,方便在读者在查看xtrace追踪日志时有基础的认知。

    图2. X11协议交互流程

    总之xtrace是Xorg X Server自带的一个工具,通过分析xtrace的输出,你可以了解X Server是如何处理客户端应用程序的请求,以及可能的问题所在。xtrace 工具的作用:

    • 调试X问题:如果你遇到了与图形界面相关的问题,例如窗口无法正常显示、图形卡驱动问题等,你可以使用xtrace来捕获X Server的活动,帮助你找出问题所在。
    • 性能分析:xtrace可以记录X Server内部的操作,帮助你分析系统的图形性能,找出潜在的瓶颈。
    • 理解X协议交互:X Server与客户端应用程序之间的通信是通过X协议进行的。xtrace可以捕获这些通信,帮助你理解应用程序与X Server之间的交互方式。
    • 功能逆向:对于依赖X11协议的软件无法查看源码,可以通过观察通信协议,逆向其部分核心功能的实现 请注意,xtrace的输出可能会非常详细,因此在使用时需要注意过滤和分析输出,以便关注于你感兴趣的信息。

    X11在文件系统中暴露了通信用的socket套接字文件,这使得第三方应用程序可以监控套接字文件从而“窥视"X11客户端和服务端之间的通信,这一技术点是xtrace工作的主要基础。

    // X11 socket套接字文件在文件系统中的位置
    ls /tmp/.X11-unix
    X0  X9
    

    如下日志所示xtrace工作的本质就是不断地获取wrote和received的数据然后将其解析为易于阅读的描述语言打印出来。

    // 全量日志
    001:>:received 32 bytes
    001:>:09dc:32: Reply to InternAtom: atom=0x250("_NET_KDE_COMPOSITE_TOGGLING")
    001:>:wrote 32 bytes
    001:>:received 32 bytes
    001:>:09dc: Event XKEYBOARD-XkbEvent(85) type=2 time=0x018d8071 device=0x03 not-yet-supported=0x10,0x00,0x00,0x10,0x01,0x00,0x00,0x00,0x00,0x01,0x90,0x10,0x10,0x10,0x90,0x00,0x01,0x90,0x11,0x00,0x00,0x87,0x05;
    001:>:wrote 32 bytes
    001:>:received 32 bytes
    001:>:09dc: Event XKEYBOARD-XkbEvent(85) type=2 time=0x018d8072 device=0x03 not-yet-supported=0x10,0x00,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x00,0x01,0x90,0x11,0x00,0x00,0x87,0x05;
    001:>:wrote 32 bytes
    001:>:received 32 bytes
    001:>:09dc: Event XKEYBOARD-XkbEvent(85) type=2 time=0x018d8073 device=0x03 not-yet-supported=0x10,0x00,0x00,0x10,0x01,0x00,0x00,0x00,0x00,0x01,0x90,0x10,0x10,0x10,0x90,0x00,0x01,0x90,0x11,0x00,0x00,0x87,0x05;
    

    其工作流程大致如下:

    • 通过套接字地址族AF_INET判断是tcp通信还是本地domain socket通信,然后通过generateSocketName或者calculateTCPport拿到addr相关的信息
    • 进入mainqueue死循环读取socket通信数据
    • 通过parse_server(c)翻译Event事件通信,然后打印输出
    • parse_client(c)翻译Requst请求通信,然后打印输出

    相关的代码调用堆栈如下:

    Breakpoint 1, startline (c=0x4f8270, d=TO_SERVER, format=0x417a71 "%04x:%3u: Request(%hhu): %s ") at parse.c:52
    52              if( (print_timestamps || print_reltimestamps)
    (gdb) bt
    #0  startline (c=0x4f8270, d=TO_SERVER, format=0x417a71 "%04x:%3u: Request(%hhu): %s ") at parse.c:52
    #1  0x000000000040b5f5 in print_client_request (c=0x4f8270, bigrequest=false) at parse.c:1692
    #2  0x000000000040cc2d in parse_client (c=0x4f8270) at parse.c:1996
    #3  0x0000000000403b4a in mainqueue (listener=4) at main.c:406
    #4  0x00000000004045d4 in main (argc=4, argv=0x7fffffffdef8) at main.c:706
    (gdb) c
    Continuing.
    
    Breakpoint 1, startline (c=0x4f8270, d=TO_CLIENT, format=0x417b32 "%04x:%u: Reply to %s: ") at parse.c:52
    52              if( (print_timestamps || print_reltimestamps)
    (gdb) bt
    #0  startline (c=0x4f8270, d=TO_CLIENT, format=0x417b32 "%04x:%u: Reply to %s: ") at parse.c:52
    #1  0x000000000040c1f3 in print_server_reply (c=0x4f8270) at parse.c:1859
    #2  0x000000000040d0e5 in parse_server (c=0x4f8270) at parse.c:2064
    #3  0x00000000004035ed in mainqueue (listener=4) at main.c:336
    #4  0x00000000004045d4 in main (argc=4, argv=0x7fffffffdef8) at main.c:706
    
    // connect socket
    Breakpoint 2, generateSocketName (addr=0x7fffffffdd30, display=9) at x11common.c:114
    114             snprintf(addr->sun_path,sizeof(addr->sun_path),"/tmp/.X11-unix/X%d",display);
    (gdb) p display 
    $5 = 9
    (gdb) c
    Continuing.
    [Detaching after fork from child process 7682]
    Got connection from unknown(local)
    
    Breakpoint 2, generateSocketName (addr=0x7fffffffd9d0, display=0) at x11common.c:114
    114             snprintf(addr->sun_path,sizeof(addr->sun_path),"/tmp/.X11-unix/X%d",display);
    (gdb) p display 
    $6 = 0
    (gdb) bt
    #0  generateSocketName (addr=0x7fffffffd9d0, display=0) at x11common.c:114
    #1  0x0000000000404c92 in connectToServer (displayname=0x7fffffffe363 ":0", family=1, hostname=0x0, display=0) at x11client.c:77
    #2  0x0000000000402766 in acceptConnection (listener=3) at main.c:95
    #3  0x0000000000403edd in mainqueue (listener=3) at main.c:452
    #4  0x00000000004045d4 in main (argc=2, argv=0x7fffffffdf18) at main.c:706
    (gdb) 
    

    协议分析

    xtrace通过拦截X11协议通信来进行分析。它捕获传输到X服务器的请求以及服务器对这些请求的响应,将他们解析化以日志输出的形式打印出来。所以本质来说协议分析指的是X11协议交互分析,需要对 X11相关的协议做到非常了解,即每个协议有什么功能,在xcb中是如何处理,在xserver中又是如何处理。受限于篇幅,笔者在此不会阐述所有的协议分析,而是拿我们平时常用的一些软件做一些分析和介绍。

    日志流关键字含义或用途
    Present-Request(148,1)客户端用于GLX等送显
    Request(1): CreateWindow请求创建X窗口,shm、glx都有
    GLX-Request(152,3): glXCreateContext请求创建GLX上下文,可以用来判断客户端是否GLX应用
    MIT-SHM-Request(130,3): PutImageX11 shm客户端请求更新图像
    Event XKEYBOARD-XkbEvent(85)xserver键盘事件传递给X客户端
    Event Generic(35) XInputExtension鼠标事件,后面带着ButtonPress、ButtonRelease、Motion等
    Request(36): GrabServergrab请求
    Request(37): UngrabServer解除grab请求
    DeleteProperty请求删除X11窗口的一些属性
    ChangeProperty改变X11窗口的一些属性
    PropertyNotify窗口属性改变发送事件通知客户端
    MIT-SHM-Request(130,4): GetImageshm方式获取屏幕图像
    Request(62): CopyArea复制屏幕一部分区域图像(离屏)
    Request(53): CreatePixmap创建图像(离屏)

    上述表格中只是介绍了常见部分的协议,X协议非常的丰富,完整的模块如下所示:

    • XCB BigRequests API 发送和接受超过请求长度(65535字节)限制的数据
    • XCB Composite API 支持窗口合成,将多个窗口的内容合成最终的显示图像
    • XCB Damage API 用于跟踪窗口或者绘图上下文的可视区域的改变
    • XCB DPMS API 用于管理显示器的电源管理功能,控制显示器电源模式
    • XCB DRI2 API 支持直接渲染和硬件加速
    • XCB DRI3 API 支持直接渲染和硬件加速,对DRI2的扩展和改进
    • XCB Glx API 提供GLX接口创建OpenGL上下文,进行图形渲染和交互
    • XCB Present API 实现高性能的图像呈现在屏幕上
    • XCB RandR API 管理显示器的分辨率、屏幕方向和显示器布局等
    • XCB Record API 记录X服务器的事件流,以便进行调试、分析等
    • XCB Render API xrender绘图
    • XCB ScreenSaver API 管理屏幕保护程序的行为和状态
    • XCB Shape API 用于创建和操作不规则窗口的形状
    • XCB Shm API 应用程序能够通过共享内存的方式高效的传输图像
    • XCB Sync API 实现同步操作和时间戳的管理,确保预期的时序
    • XCB XCMisc API 提供额外的杂项函数和功能,获取一些服务器信息
    • XCB Core API 核心部分,提供了基本通信功能和操作
    • XCB Xevie API 拦截和处理X服务器上的事件流
    • XCB XF86Dri API 直接渲染,直接访问图形硬件,提高图形性能和效率
    • XCB XFixes API 增强功能:光标、窗口形状、窗口属性、窗口位置
    • XCB Xinerama API 用于管理多个显示器的配置和操作
    • XCB Input API 处理输入事件(如键盘、鼠标、触摸屏等)交互
    • XCB xkb API 配置与操作键盘相关的设置,键盘布局和状态
    • XCB XPrint API 直接从应用程序打印文档、图像和其他内容
    • XCB API xcb基础功能
    • XCB SELinux API 在应用程序中管理selinux安全策略和执行安全操作
    • XCB Test API 测试协议,常用于远程控制
    • XCB Xv API xvideo相关,用于视频渲染、视频加速、获取视频信息
    • XCB XvMC API 在GPU上执行视频解码和运动补偿功能 笔者在工作中也有时对xtrace日志无法分析到有用信息,此时会查看XCB帮助文档,通过分析对比查找到相关的xcb函数,从而逐渐熟悉X11协议。

    总结

    总体来说xtrace是一个有用的工具,对于笔者来说经常会接触生态软件的图形显示问题,对于少部分软件开发商不愿意提供代码和问题复现最小demo,此时其软件对于笔者来说是一个黑盒,当问题边界靠近X相关的技术时,笔者会使用xtrace去详细分析该软件的详细功能,往往这可以在底层剖析软件的显示工作方式,配合系统上相关的组建库代码,可以方便地处理客户的紧急问题。 笔者编写这篇文档是希望可以鼓励更多的同事使用xtrace,这个工具可以帮助你熟悉X11的工具原理,同时也可以理解像qt、gtk等UI库的底层实现,在定位系统复制粘贴、图形显示、拖拽、窗口相关的问题时可以提供更加底层的日志,方便更加精准地定位问题根因!

    参考资料

    Wednesday, November 1, 2023

    介绍

    随着Linux图形发展的需求增长,需要更多的图形功能和性能,这促成了Direct Rending Infastructure(DRI)和Direct Rendering Manager(DRM)的出现,DRM是一个内核级别的子系统,提供了对图形设备的管理和访问控制,它允许用户空间的图形库和应用程序直接访问GPU进行渲染,而无需操作系统介入,它的主要作用如下:

    • 图形硬件管理:DRM负责管理图形硬件资源,如显存、显卡以及显示设备。它允许内核与这些硬件设备进行通信和控制。
    • 图形加速:提供对硬件加速功能的支持,例如3D渲染和视频加速。这使得图形渲染更加高效和流畅。
    • 多显示器支持:允许多个显示器的管理和配置,包括扩展桌面、镜像模式等。
    • 用户空间接口:提供了用户空间图形库(如Mesa 3D等)和应用程序与内核中DRM子系统进行通信的接口。
    • 支持不同的图形协议:DRM在支持X Window System(X11)的同时,也能与新一代图形协议,如Wayland,进行整合。

    这种更好的性能、3D加速和对硬件的更直接访问使得它可以支持X Window System和Wayland的图形输出和管理。其现状如下:

    • 持续发展:随着硬件技术和图形需求的不断演进,DRM在Linux内核中也在不断发展。新的功能和改进不断加入,以满足新一代图形硬件和应用程序的需求。
    • 多厂商支持:DRM的发展得到了多家硬件厂商的支持,这包括AMD、Intel、NVIDIA等,它们为Linux内核开发并贡献了各自硬件的DRM驱动程序。
    • 支持新技术:DRM也在逐步支持新的图形技术,例如,对于低功耗图形处理的优化、支持更高分辨率、HDR显示以及机器学习和AI加速等方面的改进。
    • Wayland和DRM集成:Wayland作为新一代图形显示协议,与DRM更为紧密地整合,提供更为直接和高效的图形渲染和显示方式。

    总体而言,现如今很多Linux发行版上图形服务器后端都是对接的drm实现图形输出和相关控制,DRM在Linux图形子系统中扮演着关键角色,随着技术和需求的不断发展,它持续进化以适应新的硬件和应用场景,为Linux系统提供了强大的图形渲染支持。

    图1. DRM时间线

    DRM剖析

    Framebuffer && DRM

    在谈drm之前必须先了解一下它的前辈Framebuffer,Framebuffer是指一块内存区域,用户存储显示设备上每个像素的颜色信息。在早期的计算机系统中,操作系统直接将图形数据写入这块显存区域,这被称为直接显存访问(Direct Framebuffer Access),这种方式简单、直接,但随着图形复杂程度增加和硬件发展,它变得难以满足高分辨率和复杂图形的需求,随着事件的推移,由DRM提供了更多先进的功能,它追加能替代了Framebuffer,并称为linux图形系统的主要组成部分(图形框架演变如下图2)。然而Frambuffer仍然在某些特定场景下有其用户之地,尤其是在一些嵌入式系统和特定的硬件上。 总的来说,Framebuffer到DRM的发展代表了图形处理在Linux系统中的进步和演进,从最初的直接访问到高级的硬件加速和更加强大的图形功能。drm为显示硬件的适配和图形的发展贡献了很大的力量。

    图2. Framebuffer到DRM

    drm-plane

    drm_plane本质是对显示控制器中scanout硬件的抽象。简单来说,给定一个plane,可以让其与一个framebuffer关联表示进行scanout的数据,同时控制scanout时进行的额外操作,比如colorspace的改变,旋转、拉伸、偏移等操作。drm_plane是与硬件强相关的,显示控制器支持的plane是固定的,其支持的功能也是由硬件决定的。所有的drm_plane必为三种类型之一:

    • Primary - 主plane,一般控制整个显示器的输出。CRTC必须要有一个这样的plane。
    • Curosr - 表示鼠标光标图层(可选),一般启用的话称其为开启硬件光标,代码如下,流程如下图3。
    • Overlay - 叠加plane,可以在主plane上叠加一层输出,可选。
    ....
    if (cursor != NULL && drm_connector_is_cursor_visible(conn)) {
    	struct wlr_drm_fb *cursor_fb = get_next_cursor_fb(conn);
    	if (cursor_fb == NULL) {
    		wlr_drm_conn_log(conn, WLR_DEBUG, "Failed to acquire cursor FB");
    		return false;
    	}
    
    	drmModeFB *drm_fb = drmModeGetFB(drm->fd, cursor_fb->id);
    	if (drm_fb == NULL) {
    		wlr_drm_conn_log_errno(conn, WLR_DEBUG, "Failed to get cursor "
    			"BO handle: drmModeGetFB failed");
    		return false;
    	}
    	uint32_t cursor_handle = drm_fb->handle;
    	uint32_t cursor_width = drm_fb->width;
    	uint32_t cursor_height = drm_fb->height;
    	drmModeFreeFB(drm_fb);
        // 设置硬件光标,更新光标图像buffer
    	int ret = drmModeSetCursor(drm->fd, crtc->id, cursor_handle,
    			cursor_width, cursor_height);
    	int set_cursor_errno = errno;
    	if (drmCloseBufferHandle(drm->fd, cursor_handle) != 0) {
    		wlr_log_errno(WLR_ERROR, "drmCloseBufferHandle failed");
    	}
    	if (ret != 0) {
    		wlr_drm_conn_log(conn, WLR_DEBUG, "drmModeSetCursor failed: %s",
    			strerror(set_cursor_errno));
    		return false;
    	}
    
        // 移动硬件光标位置
    	if (drmModeMoveCursor(drm->fd,
    			crtc->id, conn->cursor_x, conn->cursor_y) != 0) {
    		wlr_drm_conn_log_errno(conn, WLR_ERROR, "drmModeMoveCursor failed");
    		return false;
    	}
    } else {
    	if (drmModeSetCursor(drm->fd, crtc->id, 0, 0, 0)) {
    		wlr_drm_conn_log_errno(conn, WLR_DEBUG, "drmModeSetCursor failed");
    		return false;
    	}
    }
    ....
    

    图3. drm-plane框架图

    图4. 多drm-plane合成流程图

    在笔者看来,drm-plane与layer的概念类似,只是更加偏向硬件底层概念,开发者可以通过控制显示相关的参数将plane投显到显示器上任何区域(参考图5),这种映射机制提供了非常方便地偏移、缩放方法,如果在这一层上实现移动和缩放等动画,效率将会比使用OpenGL等API制作高,将频繁变化的图像抽象工作在一个单独的drm-plane也可以达到提高性能和省电的目的。

    名字描述
    SRC_X当前framebuffer crop区域的起始偏移x坐标
    SRC_Y当前framebuffer crop区域的起始偏移y坐标
    SRC_W当前framebuffer crop区域的宽度
    SRC_H当前framebuffer crop区域的高度
    CRTC_X屏幕显示区域的起始偏移x坐标
    CRTC_Y屏幕显示区域的起始偏移y坐标
    CRTC_W屏幕显示区域的宽度
    CRTC_H屏幕显示区域的高度

    表1. drm-plane部分属性表

    图5. drm-plane送显参数控制

    crtc(Cathode Ray Tube Controller)

    CRTC 是显示控制器中的一个重要部分,主要用于管理显示设备的扫描和刷新。CRTC 负责生成视频信号的定时和同步,控制屏幕上像素的扫描和刷新,以确保正确的图像显示。它管理像素的输出到屏幕上的确切位置,以及刷新率、分辨率等显示参数。在现代的图形处理中,CRTC通常由图形处理单元(GPU)或显示控制器中的专用部分来控制,以确保正确的图像输出。

    图6. crtc框架图

    drm legacy链路通过调用drmModePageFlip请求crtc更新显示器图像,其代码如下,流程如图6所示:

    static bool legacy_crtc_commit(struct wlr_drm_connector *conn,
    		const struct wlr_drm_connector_state *state,
    		uint32_t flags, bool test_only) {
    	....
    
    	if (flags & DRM_MODE_PAGE_FLIP_EVENT) {
    		uint32_t page_flags = DRM_MODE_PAGE_FLIP_EVENT;
    		if (flags & DRM_MODE_PAGE_FLIP_ASYNC) {
    			page_flags |= DRM_MODE_PAGE_FLIP_ASYNC;
    		}
    
    		if (drmModePageFlip(drm->fd, crtc->id, fb_id,
    				page_flags, drm)) {
    			wlr_drm_conn_log_errno(conn, WLR_ERROR, "drmModePageFlip failed");
    			return false;
    		}
    	}
    
    	return true;
    }
    

    图7. buffer更新流程图

    encoder

    编码器从CRTC获取像素数据,并将其转换为适合任何连接器的格式。在某些设备上,CRTC可以将数据发送到多个编码器。在这种情况下,两个编码器将从同一扫描输出缓冲区接收数据,从而在连接到每个编码器的连接器上产生“克隆”显示配置。 在Linux系统中,DRM(Direct Rendering Manager)是用于处理图形显示的子系统,负责在用户空间和图形硬件之间提供接口。DRM编码器在Linux中扮演了关键角色,主要用于对图形和视频内容进行编码、解码、处理和显示。在DRM框架中,编码器的作用包括但不限于:

    • 视频压缩和解压缩: 编码器负责对视频内容进行压缩,以减小数据量并更有效地传输和存储视频流。解码器用于解压缩视频内容,以便图形硬件能够正确地渲染和显示内容。
    • 图形处理和渲染: DRM编码器能够处理图形和视频流,进行渲染和合成,然后将图像数据发送到显示设备以在屏幕上显示。
    • 硬件加速: DRM编码器还可以利用硬件加速功能,以便更快地处理图形和视频内容。这有助于提高性能和效率,特别是对于高分辨率视频或图形内容的处理。
    • 支持多种编解码标准: DRM编码器能够支持多种视频编解码标准,如H.264、H.265等,确保对不同格式的视频流进行正确的处理。 在Linux系统中,DRM编码器与图形驱动程序、图形处理单元(GPU)和显示设备等硬件密切相关。它允许Linux系统管理和控制图形硬件,处理视频流并在屏幕上显示图像,同时也与DRM系统中的访问控制和安全性机制集成,确保受保护内容的安全传输和显示。

    图8. encoder框架图

    connector

    DRM connectors是用于管理和描述图形设备的连接器或端口。它们提供了对显示设备的连接和属性描述。在图形系统中,这些连接器可以代表诸如HDMI、DisplayPort、DVI等物理连接接口,而它们也可以描述内部连接,例如LCD panels。DRM connectors 在 Linux 中的作用包括:

    • 显示设备连接描述: DRM connectors 提供了关于显示设备的物理连接信息,比如类型(HDMI、VGA、DisplayPort等)、连接状态以及支持的分辨率和刷新率等信息。
    • 动态连接管理: 它们可以监测连接和断开事件。当显示设备插入或拔出时,DRM connectors 可以检测到这些变化,并通知系统相应的变化。这使系统能够动态调整图形配置以适应新的连接或断开状态。
    • 多显示器支持: DRM connectors 允许系统管理多个显示器的连接和配置。这使得可以同时使用多个显示器或监视器,或者在不同的显示设备上显示不同的内容。
    • 显示属性查询和配置: 通过 DRM connectors,可以查询连接器支持的分辨率、刷新率以及其他显示属性。这使系统可以调整和配置图形设备,以最佳方式显示图形内容。

    总体来说,DRM connectors 在 Linux 中的作用是管理显示设备的物理连接、状态和属性,使系统能够动态适应不同的显示设备、支持多个显示器,并提供相应的配置信息,以便正确地显示图形内容。

    图9. connector框架图

    framebuffer

    framebuffer是一个重要概念。Framebuffers是用于存储屏幕上每个像素的颜色和其他相关信息的内存区域。在DRM中,framebuffer提供了一个抽象接口,允许用户空间程序访问和操作显存,以便渲染图形数据。其作用包括:

    • 图形数据存储: Framebuffers提供一个区域,用于存储图形数据,包括每个像素的颜色、透明度等信息。这些数据构成了屏幕上显示的图像。
    • 直接访问屏幕数据: 通过framebuffers,用户空间程序或操作系统内核能够直接访问和操作图形数据,而无需经过额外的复杂处理。
    • 硬件抽象层: Framebuffers提供了一个硬件无关的抽象接口,这意味着不同的图形设备和硬件都可以通过相同的接口进行访问和操作。
    • 显示图像: Framebuffers存储了最终用于在屏幕上显示的图像数据。图形数据在 framebuffer中进行组织和处理,然后通过显示控制器输出到屏幕。

    在DRM 中,framebuffers通常与CRTC和显示控制器等组件一起工作。framebuffers提供的抽象层允许操作系统或应用程序以统一的方式对图形数据进行处理和管理,无论具体的硬件设备是什么样的。总之,framebuffers 在 Linux 的 DRM 中提供了一个接口和数据结构,用于存储、操作和管理图形数据,使其能够在屏幕上正确地显示图像。

    图10. framebuffer内存结构图

    drm调试工具

    drm_info

    用于转储有关 DRM 设备信息的小实用程序

    drm_info
    
    Node: /dev/dri/card1
    ├───Driver: i915 (Intel Graphics) version 1.6.0 (20201103)
    │   ├───DRM_CLIENT_CAP_STEREO_3D supported
    │   ├───DRM_CLIENT_CAP_UNIVERSAL_PLANES supported
    │   ├───DRM_CLIENT_CAP_ATOMIC supported
    │   ├───DRM_CLIENT_CAP_ASPECT_RATIO supported
    │   ├───DRM_CLIENT_CAP_WRITEBACK_CONNECTORS supported
    │   ├───DRM_CAP_DUMB_BUFFER = 1
    │   ├───DRM_CAP_VBLANK_HIGH_CRTC = 1
    │   ├───DRM_CAP_DUMB_PREFERRED_DEPTH = 24
    │   ├───DRM_CAP_DUMB_PREFER_SHADOW = 1
    │   ├───DRM_CAP_PRIME = 3
    │   ├───DRM_CAP_TIMESTAMP_MONOTONIC = 1
    │   ├───DRM_CAP_ASYNC_PAGE_FLIP = 1
    │   ├───DRM_CAP_CURSOR_WIDTH = 256
    │   ├───DRM_CAP_CURSOR_HEIGHT = 256
    │   ├───DRM_CAP_ADDFB2_MODIFIERS = 1
    │   ├───DRM_CAP_PAGE_FLIP_TARGET = 0
    │   ├───DRM_CAP_CRTC_IN_VBLANK_EVENT = 1
    │   ├───DRM_CAP_SYNCOBJ = 1
    │   └───DRM_CAP_SYNCOBJ_TIMELINE = 1
    ├───Device: PCI 8086:46aa Intel Corporation Alder Lake-UP4 GT2 [Iris Xe Graphics]
    │   └───Available nodes: primary, render
    ├───Framebuffer size
    │   ├───Width: [0, 16384]
    │   └───Height: [0, 16384]
    ├───Connectors
    │   ├───Connector 0
    │   │   ├───Object ID: 236
    │   │   ├───Type: eDP
    │   │   ├───Status: connected
    │   │   ├───Physical size: 290x180 mm
    │   │   ├───Subpixel: unknown
    │   │   ├───Encoders: {0}
    │   │   ├───Modes
    │   │   │   └───[email protected] preferred driver phsync nvsync 
    │   │   └───Properties
    │   │       ├───"EDID" (immutable): blob = 266
    │   │       ├───"DPMS": enum {On, Standby, Suspend, Off} = On
    │   │       ├───"link-status": enum {Good, Bad} = Good
    │   │       ├───"non-desktop" (immutable): range [0, 1] = 0
    │   │       ├───"TILE" (immutable): blob = 0
    │   │       ├───"CRTC_ID" (atomic): object CRTC = 80
    │   │       ├───"scaling mode": enum {Full, Center, Full aspect} = Full aspect
    │   │       ├───"panel orientation" (immutable): enum {Normal, Upside Down, Left Side Up, Right Side Up} = Normal
    │   │       ├───"Broadcast RGB": enum {Automatic, Full, Limited 16:235} = Automatic
    │   │       ├───"max bpc": range [6, 12] = 10
    │   │       ├───"Colorspace": enum {Default, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, RGB_WIDE_FIXED, RGB_WIDE_FLOAT, BT601_YCC} = Default
    │   │       ├───"HDR_OUTPUT_METADATA": blob = 0
    │   │       └───"vrr_capable" (immutable): range [0, 1] = 0
    │   ├───Connector 1
    │   │   ├───Object ID: 245
    │   │   ├───Type: DisplayPort
    │   │   ├───Status: disconnected
    │   │   ├───Encoders: {1}
    │   │   └───Properties
    │   │       ├───"EDID" (immutable): blob = 0
    │   │       ├───"DPMS": enum {On, Standby, Suspend, Off} = Off
    │   │       ├───"link-status": enum {Good, Bad} = Good
    │   │       ├───"non-desktop" (immutable): range [0, 1] = 0
    │   │       ├───"TILE" (immutable): blob = 0
    │   │       ├───"CRTC_ID" (atomic): object CRTC = 0
    │   │       ├───"subconnector" (immutable): enum {Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native} = Unknown
    │   │       ├───"audio": enum {force-dvi, off, auto, on} = auto
    │   │       ├───"Broadcast RGB": enum {Automatic, Full, Limited 16:235} = Automatic
    │   │       ├───"max bpc": range [6, 12] = 12
    │   │       ├───"Colorspace": enum {Default, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, RGB_WIDE_FIXED, RGB_WIDE_FLOAT, BT601_YCC} = Default
    │   │       ├───"HDR_OUTPUT_METADATA": blob = 0
    │   │       ├───"vrr_capable" (immutable): range [0, 1] = 0
    │   │       ├───"Content Protection": enum {Undesired, Desired, Enabled} = Undesired
    │   │       └───"HDCP Content Type": enum {HDCP Type0, HDCP Type1} = HDCP Type0
    │   └───Connector 2
    │       ├───Object ID: 258
    │       ├───Type: DisplayPort
    │       ├───Status: disconnected
    │       ├───Encoders: {6}
    │       └───Properties
    │           ├───"EDID" (immutable): blob = 0
    │           ├───"DPMS": enum {On, Standby, Suspend, Off} = Off
    │           ├───"link-status": enum {Good, Bad} = Good
    │           ├───"non-desktop" (immutable): range [0, 1] = 0
    │           ├───"TILE" (immutable): blob = 0
    │           ├───"CRTC_ID" (atomic): object CRTC = 0
    │           ├───"subconnector" (immutable): enum {Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native} = Unknown
    │           ├───"audio": enum {force-dvi, off, auto, on} = auto
    │           ├───"Broadcast RGB": enum {Automatic, Full, Limited 16:235} = Automatic
    │           ├───"max bpc": range [6, 12] = 12
    │           ├───"Colorspace": enum {Default, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, RGB_WIDE_FIXED, RGB_WIDE_FLOAT, BT601_YCC} = Default
    │           ├───"HDR_OUTPUT_METADATA": blob = 0
    │           ├───"vrr_capable" (immutable): range [0, 1] = 0
    │           ├───"Content Protection": enum {Undesired, Desired, Enabled} = Undesired
    │           └───"HDCP Content Type": enum {HDCP Type0, HDCP Type1} = HDCP Type0
    ├───Encoders
    │   ├───Encoder 0
    │   │   ├───Object ID: 235
    │   │   ├───Type: TMDS
    │   │   ├───CRTCS: {0, 1, 2, 3}
    │   │   └───Clones: {0}
    │   ├───Encoder 1
    │   │   ├───Object ID: 244
    │   │   ├───Type: TMDS
    │   │   ├───CRTCS: {0, 1, 2, 3}
    │   │   └───Clones: {1}
    
    .........
    
    ├───CRTCs
    │   ├───CRTC 0
    │   │   ├───Object ID: 80
    │   │   ├───Legacy info
    │   │   │   ├───Mode: [email protected] preferred driver phsync nvsync 
    │   │   │   └───Gamma size: 256
    │   │   └───Properties
    │   │       ├───"ACTIVE" (atomic): range [0, 1] = 1
    │   │       ├───"MODE_ID" (atomic): blob = 271
    │   │       │   └───[email protected] preferred driver phsync nvsync 
    │   │       ├───"OUT_FENCE_PTR" (atomic): range [0, UINT64_MAX] = 0
    │   │       ├───"VRR_ENABLED": range [0, 1] = 0
    │   │       ├───"SCALING_FILTER": enum {Default, Nearest Neighbor} = Default
    │   │       ├───"DEGAMMA_LUT": blob = 0
    │   │       ├───"DEGAMMA_LUT_SIZE" (immutable): range [0, UINT32_MAX] = 129
    │   │       ├───"CTM": blob = 0
    │   │       ├───"GAMMA_LUT": blob = 270
    │   │       └───"GAMMA_LUT_SIZE" (immutable): range [0, UINT32_MAX] = 1024
    
    ........
    
    
    └───Planes
        ├───Plane 0
        │   ├───Object ID: 31
        │   ├───CRTCs: {0}
        │   ├───Legacy info
        │   │   ├───FB ID: 237
        │   │   │   ├───Object ID: 237
        │   │   │   ├───Size: 2880x1800
        │   │   │   ├───Format: ARGB2101010 (0x30335241)
        │   │   │   ├───Modifier: I915_FORMAT_MOD_Y_TILED (0x100000000000002)
        │   │   │   └───Planes:
        │   │   │       └───Plane 0: offset = 0, pitch = 11520 bytes
        │   │   └───Formats:
        │   │       ├───C8 (0x20203843)
        │   │       ├───RGB565 (0x36314752)
        │   │       ├───XRGB8888 (0x34325258)
        │   │       ├───XBGR8888 (0x34324258)
        │   │       ├───ARGB8888 (0x34325241)
        │   │       ├───ABGR8888 (0x34324241)
        
        .....
    

    drm_monitor

    用于监控 KMS 状态的 CLI 工具

    drm_monitor -d /dev/dri/card1
    CRTC 80: seq=30095325875 ns=3076787603594 delta_ns=16668012 Hz=59.995157
    CRTC 131: seq=0 ns=0 delta_ns=0 Hz=0.000000
    CRTC 182: seq=0 ns=0 delta_ns=0 Hz=0.000000
    CRTC 233: seq=0 ns=0 delta_ns=0 Hz=0.000000
    

    参考资料

    Tuesday, October 31, 2023

    一、背景

    近期,我们收到用户反馈,在使用 deepin 系统过程中遇到了 CPU 功耗过高导致的设备发热、续航较差情况,而用户在这些负载场景下,CPU 的占用往往不高。为了解决这个痛点,统信软件开源社区中心特别成立专项计划,对于 deepin 的电源进行专项优化,本文旨在针对此问题根因进行分析于说明。

    在对电源进行专项优化之前,我们首先对 deepin 系统进行了深入的调查和分析,以了解其在负载场景下的实际运行情况。经过对 CPU 使用率和功耗的监测,我们发现了一个令人惊讶的事实:尽管在高负载场景下 CPU 的占用率不高,但其功耗却持续升高,最终导致设备发热并影响续航。也就是说,我们前期做的省电优化工作,不仅无效,还起了反作用(具体情况作者将在下文作出仔细说明)。

    二、问题

    1. 内核

    最开始发现问题的地方在内核。有用户将我们的内核和 ubuntu 的内核进行对比后发现,虽然我们的系统和 ubuntu 系统的性能差不多,但是在发热和续航上,我们较 ubuntu 落后较多。有用户在 deepin 系统上使用 ubuntu 和其他开源 linux 发行版的配置文件分别编译内核,发现 deepin 的主要问题存在于发热控制之上。我们的测试同事高度重视这一社区反馈,遂对社区用户反馈的问题进行复现,佐证了这一现象。

    对于这种问题,我们立即联系了内核研发部的同事,并邀请部分对内核配置有一定研究的社区用户共同参与。在大家的合力排查下,我们发现,deepin v23 中提供的 HWE 内核存在部分 debug 和无用的内核选项被开启的情况,并且部分节电功能实际未能获得启用,这些都在一定程度上导致了 deepin v23 的续航表现不佳。

    2. 系统

    在系统层面,我重新审视了 dde-daemon 提供的电源调度模块,并且对比内核文档提供的文件接口,分析我们用户使用的电源模式,发现其中存在可以优化的空间。这将是本文着重讲解的内容之一。

    三、前置知识

    1.ACPI

    ACPI 是 Advanced Configuration and Power Interface 的缩写,是一种计算机硬件和操作系统之间交换能源相关信息的接口规范。它定义了计算机硬件的能源相关信息,如电源供应器状态、设备功耗、设备功率因数等。ACPI 是操作系统控制计算机硬件能源管理的标准,同时也是硬件厂商和操作系统之间通信的标准。

    在 deepin 系统中,ACPI 负责处理计算机硬件的能源管理,它与 deepin 系统的电源管理模块进行交互,以实现对计算机硬件的能源管理。

    在分析系统层面的问题时,我们需要了解 ACPI 和电源管理模块的作用和功能,以及它们是如何协同工作的。在本文中,我们将会详细讲解 ACPI 的工作原理以及 deepin 系统中的电源调度模块工作模式,并提出可行优化建议。

    首先,让我们了解一下 ACPI 的工作原理。当计算机硬件发生电源变化时,ACPI 会收集硬件信息,并向操作系统发送电源请求。操作系统收到电源请求后,会根据用户配置自动调整各个硬件的电源策略。而 deepin 系统的电源模块则是帮助用户生成配置来调整 ACPI 的行为。所以在这一方面,我们能做的就是向 ACPI 提供合理的电源策略,在保证性能的同时,降低设备温度并提升续航表现。

    2.平台电源配置

    平台电源配置提供了三种可选模式:performance(性能模式),balance(平衡模式),low-power(节能模式)。一般情况下,用户使用平衡模式就可以。在台式机和 mini 主机类(对于功耗和发热没有任何要求)设备上默认提供性能模式,在笔记本等移动设备上默认提供平衡模式。默认不提供节能模式,因为某些 ACPI 设备在节能模式工作过程中可能出现“睡死现象”,所以为了避免此问题,默认不提供 low-power 节电模式。

    3.CPU 电源配置

    /sys/devices/system/cpu/cpufreq目录下有许多文件名为policy<x>(x 代表核心编号),这些文件对应着你电脑上的 CPU 核心,而 CPU 的电源调度细节就在这些文件夹里面。在policy<x>目录下有一个文件`scaling_driver``,使用 cat 或其他方式访问它,得到的结果就是我们当前使用的调度器:

    • intel_cpufreq / acpi_cpufreq : 使用 scaling freq 调度
    • intel_pastate : 使用 Intel Pstate 调度
    • amd-pstate : 使用 AMD Pstate 调度

     scalling freq 调度

    这是最传统的 CPU 调度方式,你可以在 policy文件夹下的 scaling_available_governors 获取可选电源模式:

    英文中文含义
    performance性能模式最极致的性能表现,最火热的 CPU 温度,最短的续航。
    powersave节能模式为绿色地球出一份力。
    balance平衡模式性能和续航兼顾。小孩子才做选择,我全都要。
    schedutil平衡模式平衡模式的一种,使用不同算法进行调度。
    ondemand平衡模式平衡模式的一种,根据当前 CPU 负载动态调整频率。当负载大于阈值时调整到最高频率,其他情况按负载比例计算频率。
    conservative平衡模式平衡模式的一种,根据当前 CPU 负载动态调整频率。当负载大于最大阈值时步进递增频率,当负载小于最低阈值时步进递减。
    userspace用户模式以用户指定的频率运行 CPU,可通过/sys/devices/system/cpu/cpuX/cpufreq/scaling_setspeed 进行配置

    你可能好奇,为啥这里有这么多平衡模式,其实这些平衡模式的作用都是是一样的:平衡性能和续航,不过使用的算法可能不同,这里我不做详细说明,我在网络上找到一些详细资料可以参考,有兴趣的朋友可自行查阅:

     Intel Pstate

    这是 Intel 近几代 CPU 独享的 moment,内核开启 intel pstate 后(V23 内核默认开启)你会发现在 policy文件夹下多了几个文件:

    我们只需要关注:

    • energy_performance_available_perference : 可用的 pstate 电源调度
    • energy_performance_perference:当前选定的 pstate 电源调度,可以更改此文件内容来更改电源调度
    • 在 Intel Pstate 中出现了两个新的调度方案:
    • balance_performance : 平衡偏性能,平时工作频率不高,在负载增大时能快速响应
    • balance_power : 平衡偏节能,电源策略较为保守 在部分电脑上还有 default 方案,此方案就是经过 pstate 优化过的 balance 策略。具体 PState 使用的黑魔法以及主动模式和被动模式的调度策略,可以参照内核文档进行分析。

    AMD PState

    这是 AMD ZEN2 以上用户,以及支持 kernel 6.4.x 用户独享的 moment。其实 AMD 在 6.1 内核已经做了 PState 的支持,不过是被动模式。

    • (Actvie Mode)主动模式

      Active Mode 仅在内核版本大于 6.4 以上,且内核选项打开 AMD PState 时可用。可能需要在 grub 内加入启动参数以打开此功能:amd_pstate=active,也可以修改文件实现 Active Mode 的电源策略和 Intel PStatewi 类似。

    • (Passive Mode)被动模式

      Passtive Mode 仅在内核大于 6.1 以上,且内核选项打开 AMD PState 时可用。可能需要在 grub 加入启动参数开启此功能:amd_pstate=passive,也可修改文件实现。

      Passive Mode 提供两种电源模式,在/sys/device/system/cpu/cpufreq/scaling_governor文件进行调整:

      • performance 使用 platform_profile 进行配置,调度积极性较高
      • scheutils 在/sys/device/system/cpu/cpufreq/schedutil/rate_limit_us文件中调整调度粒度(两次调度的间隔时间)和 ACPI 的 scheutils 类/sys/device/system/cpu/cpufreq/scaling_governor
    • (Guided Mode)引导模式

      Guided Mode 仅在内核大于 6.1 以上,且内核选项打开 AMD PState 时可用。可能需要在 grub 加入启动参数开启此功能:amd_pstate=guided,也可修改文件实现。这就类似汽车的自动挡,驱动程序请求最低和最大性能级别,平台自动选择此范围内适合当前工作负荷的性能级别。

    4.GPU 电源管理部分

     AMD GPU

    如果是 AMD GPU 则需要更改两个文件(使用 tee 命令进行写入):

    • /sys/class/drm/card0/device/power_dpm_state(这是一个遗留接口,目的是向后兼容)
    • performance 高性能模式
    • balance 平衡模式
    • battery 节能模式
    • /sys/class/drm/card0/device/power_dpm_force_performance_level

    以下设置来自 AMD 官方驱动文档:

    https://dri.freedesktop.org/docs/drm/gpu/amdgpu.html#power-dpm-force-performance-level

    drm/amdgpu AMDgpu driver — The Linux Kernel  documentation

    power_dpm_force_performance_level:

    AMD GPU 驱动程序提供了一个 sysfs API,用于调整某些与功率相关的参数。文件 power-dpm-force-performance-level 将用于执行此操作。它接受以下参数:

    • auto:当选择 auto 时,设备将尝试针对驱动中的当前条件动态选择最佳功率曲线
    • low:当选择低时,GPU 被强制到最低功率状态
    • high:当选择高时,GPU 被强制到最高功率状态
    • manual:当选择手动时,用户可以通过 sysfs pp_dpm_mclk、pp_dpm_sclk 和 pp_dpm_pcie 文件手动调整每个时钟域启用的电源状态,并通过 pp_power_profile_mode sysfs 文件调整电源状态转换方式。
    • profile_standard 固定时钟级别分析模式。此模式将时钟设置为固定级别,该级别因 ASIC 而异。这对于分析特定工作负载很有用(不常用)。
    • profile_min_sclk 最小 sclk 分析模式。此模式将 sclk 强制设置为最低级别。这对于分析最小功耗的场景很有用(不常用)。
    • profile_min_mclk 最小 mclk 分析模式。此模式将 mclk 强制设置为最低级别。这对于分析最小功耗的场景很有用(不常用)。
    • profile_peak 峰值分析模式。此模式将所有时钟(mclk、sclk、pcie)设置为最高级别。这对于分析最大性能的场景很有用(不常用)。

    测试:

    • LOW 模式的跑分

    LOW 模式的跑分

    • auto 模式的跑分

    auto 模式的跑分

    • high 模式的跑分

    high 模式的跑分

     Intel GPU

    intel GPU 使用的 i915 驱动,并不希望你对其做出调整,因为其驱动自带的电源策略已经足够聪明。不过你也可以通过 intel 提供的 intel-gpu-tools 进行调整和获取信息。

    sudo apt install intel-gpu-tools

    然后使用

    sudo intel_gpu_frequency

    来获取当前频率(当前使用的是 Intel A750)

    可以看到 intel 的显卡驱动是在 600 MHz 到 2400 MHz 之间动态调整(如上图)

    测试笔记本下 intel 核显跑分如下 图片

    Nvidia

    由于 nvidia 驱动不开源,所以在系统层面无法对其做控制。

    四、应用

    应用级别的省电,应该就是在保证用户使用流畅度的情前提下下节省性能。之前也有用户提出过,是否能参照 vivo 的 origin3 os 的不公平调度算法来实现优化。毕竟安卓系统的底层也是 linux,理论上实现难度不大。

    Cgroups,全称 Control Groups,是 Linux 内核提供的一种资源管理机制,用于对进程分组并对其资源进行限制和隔离。Cgroups 可以用于限制进程的 CPU、内存、磁盘、网络等资源,也可以用于限制进程的优先级和 IO 权限。利用其提供的能力,我们很容易实现类似不公平调度算法(我们新的 AM 天然支持 Cgroups 的操作),但是我还有一些顾虑:

    • 不同于手机操作系统,计算机操作系统是多任务并行的,在多数窗口管理器下,我们并没有一个明显的前台应用,此时使用不公平调度可能存在隐患;
    • 容易引发人机对抗。在我的观念里面,计算机是为人服务的,那么用户的意志必定是第一优先级,所以我们不应改变用户的行为,如果使用不平衡调度和用户预期不一致,会极大降低用户体验;
    • 使用前后台区分应用,可能导致开销和收益比下降,性价比不高。linux 桌面不像安卓设备有明显前后台,那么用户频繁切换应用的操作将导致我们的调度器频繁切换调度,使得开销过大。 我认为最佳的解决方案是:提供能力,但不提供方案。我们可以提供基于 Cgroups 方式修改应用组的优先级,然后让用户自己选择什么应用优先级更高,什么应用优先级低,以实现调度(比如在 dock 上右键选择优先级)或提供一套配置以供用户自由选择。

    如果一个电脑需要使用不平衡调度来保证使用流畅性,可能这并不是一个操作系统能解决的问题,而更应该考虑硬件是否需要更换,以保证多任务使用的流畅性。

    附录——常用的调试测试工具

    1. S-tui

    可以看到 CPU 频率变化,配合 stress 可以对 cpu 进行压力测试。 图片

    2. intel-gpu-tools

    可以使用 intel_gpu_frequency 来获取和调整 i965 的驱动频率。

    3. glmark2

    GPU 跑分软件。

    4. stress-ng

    CPU 压力测试软件。

    5. powertop

    电源测试软件,可以看到电源的功耗和使用情况。

    Sunday, September 24, 2023

    随笔

    在给Wireshark做完编译、调整、上架之后,我打算到论坛里看看大家对开源应用有没有什么疑惑的地方,没想到大家现在对开源的关注度这么高了。在看到最近一些闹得比较沸沸扬扬的话题之后,我也对我现在开源应用适配工作的很多地方产生了疑惑,我这样跟开源还有关系吗?

    重新适配开源应用的意义

    在一些实际的场景中,其实有很多"重复造轮子"的情况,即明确存在其他开源项目可用来替代的情况下仍然自己重头造一个项目来实现相同/类似的功能。比如各大桌面环境都有自己的一套文管:DDE的dde-file-manager、KDE的Dolphin,但严格意义上他们不是重复造轮子,而是更好地融入桌面环境中。 另外一种情况就是我现在做的适配开源应用,我的作品很大一部分可以在系统的应用商店中找到。但细心的小伙伴可能会发现:

    • “诶,这个应用不是系统仓库里有吗?为什么还要消耗资源去重复适配?”

    • “Debian 12的这个应用已经去到3.6.1了,为什么商店里的还是3.6.0?”

    • “为什么软件官方已经提供了deepin可以直接安装的包,应用商店还要另起一个新包名互不兼容?”

      想到这几个问题,大家是不是血压都上来了?这不就是妥妥重复劳动吗?别急,听我慢慢道出里面的小细节。我愿总结为两个核要义:平易、近人。

    易用性

    首先问大家一个问题,抛开命令真爱粉不谈,你认为用apt安装包方便还是在应用商店看着截图、应用描述、评论等等来一站式点击"安装"方便? 虽然命令也可以直接安装各种系统仓内的各种包,但对于新手用户个人认为还是有一定难度的。而且不使用应用商店来下载,很有可能就与应用商店各种特性擦肩而过了,比如定期的应用推荐、应用版本迭代的changelog。

    用户侧体验优化

    不知道大家有没有体验最近Wireshark-4.0.8,有使用过的朋友应该会发现我们很贴心的准备了两个desktop启动方式,一个是普通模式一个是root模式,中间的取舍故事今天就不多赘述了,大家有兴趣可以后面单独拎出来。 至于为什么会设置这么两个模式?其实我在适配开源项目前一般都会预览大家在应用商店里的反馈,这次的举动主要是由于Wireshark用普通user权限可以打开程序,但只是能看个壳子,只有root权限打开才是完整的功能。而且大家也反馈了比较多这个情况,我在评估一段之间、测试可行性之后给大家带来了"两个desktop+提示窗+双语支持"的独家体验。 “两个desktop+提示窗"是为了让大家方便以不同的模式打开,“提示窗"则是根据不同模式给大家展示相关提示信息,“双语支持"则是根据系统语言来设置中文和其他语言下分别显示中文提示和英文提示,一定程度上兼顾海外友人的体验。

    image

    我观察了下,Wireshark编译之后的原版desktop文件是不提供这种级别的优化的;而现在deepin应用商店里的版本实实在在可以做到了这个层次的优化,亲近人心。 大家可以把细节打在评论上。

    生态多样性

    严格意义上,每个包只有包名是相对唯一的,这也是因为大部分包管理器都会通过包名来检查该包的情况。但对于同一个应用特别是开源项目而言,它允许被多次修改分发,除了用版本号用以区分,也并没有强制要求每个人维护的包名都遵循同一个。 但为什么Debian等主流发行版一般包名都会随着版本迭代而保留使用,一般并不轻易修改包名?个人认为有以下几个可能:

    • 一般主流发行版仓库中某个重要项目/库为了保证长期稳定性和可维护性,一般都由同一个维护者负责维护,所以包名不会发生明显改动。

    • 包名变动过大,相关联的其他包/库均需要重新调整依赖关系,不太利于用户体验和仓库维护管理。

    • 接第一个情况,即便每个人都有编译开源项目的自由,但大家都比较自觉不会向系统仓中投递已经确切存在的包。

      除此之外,如果你同时在应用商店和外部安装一个包名一致的应用,则只会保留版本较新的一个。或者说,如果你想要保留本地安装的版本,但你在应用商店里对其进行升级,那此时你本地的包就会被替换掉了。

    稳定性测试

    其实在开源应用上架到应用商店前都会有测试流程,大家也很难保障哪一个版本会有致命问题,或者哪一个版本会丧失预期特性。所以我们在选包之后,一般是先编译一遍,没有致命问题我们才进行其他操作,尽最大努力保驾护航。

    适配开源应用的难度

    要是你觉得我们就是单纯把一些官方提供的二进制归档文件甚至是直接可用的deb包转化来上架应用商店,比如Visual Studio Code。那我很坦白告诉你,就是单纯几个字,“解包–运行–重新打包”。 但如果你是看了上文内容的话,就知道事情并不简单了。不只是从源码开始编译,核心在于如何与终端用户、各大开发者站在统一战线。当然,这里指的并不只是不断去满足用户的需要,这种可能会比较适合C端方向。我更希望的,是帮助用户在专业性比较强的问题面前能够提供满意的答卷,比如这次Wireshark,其实大部分用户真的不一定知道需要使用root来运行,所以我们做了优化。 应用/软件包只是一个普通产出物,不可替代的其实是整个适配过程中与其他人产生差异化的用户思维。

    结语

    曾经,我觉得我适配出来的开源应用只上架应用商店确实是玷污了开源,而且我并不是对程序Core本身做出了代码性贡献,这些优化充其量可能也就是普通建议而已。但后来,能看到大家对开源的支持,我也释怀了。 我希望以后有机会可以分享我在编译中发生的故事,同时也很支持使用其他发行版的小伙伴可以使用我在deepin上编译、优化调整之后的开源应用。 我也希望在开源这片蓝天里,我不是孤军奋战。

    Friday, September 22, 2023

    前不久深度科技旗下deepin社区发布了自己的 IDE:deepin-IDE,得到了全网用户尤其是开源社区用户的广泛关注,目前在 GitHub(https://github.com/linuxdeepin/deepin-unioncode)仓库的 star 数量已经达到 600 多个,说明大家的热情还是很高涨的。

    为了从技术层面给大家的热情做一个反馈,本文试着将 deepin-IDE 内部的一些实现方法进行分享,希望能够解答友友们的疑惑并得到积极的反馈。

    本篇挑了大家关心的“调试”部分进行分享。需要说明的是,deepin-IDE 的调试功能是选用 DAP(Debug Adapter Protocol )调试适配协议实现的,所以整体架构是围绕该协议搭建的,至于 DAP 具体是什么,让我们带着问号往下看。

    什么是 DAP 协议

    DAP 即调试适配协议( Debug Adapter Protocol ),顾名思义,它是用来对多种调试器进行抽象统一的适配层,将原有 IDE 和调试工具直接交互的模式更改为和 DAP 进行交互。该模式可以让 IDE 集成多种调试器变得更简单,且灵活性更好。

    IDE 中的调试功能有许多小功能组成,包括单步执行、断点、查看变量值等,常规的实现方式是在每个 IDE 中去实现这些逻辑,且因为调试工具的接口不同,还需要为每个调试工具做一些适配工作,这将导致大量且重复的工作,如下图所示:

    调试适配器协议背后的想法是标准化一个抽象协议,用于开发工具如何与具体调试器通信。这个思想和 LSP(Language Server Protocol)和 BSP(Build Server Protocol)类似,都是通过协议去统一相同功能在不同工具之间的差异性。其所处位置如下图所示,其中左边为不同的开发工具,右边为不能同的调试器,不同于开发工具和调试器直接交互的方式,DAP 将这些交互统一了起来,让开发工具和调试工具都面向 DAP 编程。

    上图中的交互是通过协议进行,所以不会像通过 API 的方式存在语言限制,可以更好的适应调试器的集成。

    DAP 如何工作

    以下部分解释了开发工具(例如 IDE 或编辑器)和调试适配器之间的交互,包括具体的协议格式说明、交互流程等。

    调试会话

    开发工具有两种基础的方式和调试器进行交互,分别是:

    【单会话模式】

    在这种模式下,开发工具启动一个调试适配器作为一个单独的进程并且通过标准的std接口进行通信。在调试会话的结束时调试适配器就终止,对于当前的调试会话,开发工具往往需要实现多个调试适配。

    【多会话模式】

    在这种模式下,开发工具不会启动调试适配器,而是假定它已经在运行并且会在特定端口上侦听连接尝试,对于每个调试会话,开发工具在特定端口上启动一个新的通信会话并在会话结束时断开连接。

    在与调试适配器建立连接后,开发工具和调试适配器之间通过基础协议进行通信。

    基础协议

    基础协议由两部分组成,包括头和内容(类似于 HTTP),头部和内容部分通过“\r\n”进行分割:

    【协议头】

    协议头部分由字段组成, 每个头字段由一个键和一个值组成,用‘:’(一个冒号和一个空格)分隔, 每个头字段都以“\r\n“结尾。由于最后一个协议头字段和整个协议头本身都以 \r\n 终止,并且由于协议头是强制性的,所以消息的内容部分总是在(并唯一标识)两个 \r\n 序列之前。当前只支持一个协议头字段:

    头字段名值类型描述
    Content-Length数字这个字段是必须的,用来记录内容字段的长度,单位是字节。

    协议头部分使用的是“ASCII”编码。

    【内容部分】

    内容部分包含了实际要传输的数据,这些数据用 JSON 格式来描述请求、响应和事件。内容部分用的是 utf-8 编码

    为了有个具体的认识,这里举个简单的例子。在调试过程中,开发人员经常会使用到下一步操作,在 DAP 中其协议为:

    Content-Length: 119\r\n
    \r\n
    {
        "seq": 153,
        "type": "request",
        "command": "next",
        "arguments": {
            "threadId": 3
        }
    }
    

    类型是“请求”,命令是下一步,参数部分可以携带多个,这里是用的线程Id。 这个协议看着挺简单的,是吧?接下来就讲讲如何使用它。

    使用方法

    详细的使用方法这里就不涉及,因为用一个时序图就可以说明:

    可以看到,初始化、请求、响应等必要的步骤都在图中。其中调试适配器可以理解为调试器的抽象,调试功能的最终执行者是由对应语言的调试工具实现的。

    在 deepin-IDE 中的实现

    在 deepin-IDE 中,调试功能的实现是结合 cppdap + debugmanager 实现的。

    cppdap 是一款基于 C++ 开发的 SDK,基本实现了 DAP 的全量协议。 deepin-IDE 的客户端和服务端都是应用的该 SDK 进行开发,据此可以实现以下功能:

    1.通信功能,包括服务端的 TCP 监听,客户端的 TCP 连接等;

    2.DAP 协议的封装,并实现协议的串行化和解串行化;

    3.提供注册回调功能,从而可以在回调内处理各种事件、请求等;

    它的层级结构如下:

    cppdap 可以减少客户端和服务端不少工作量,也统一了两边的协议数据。而 debugmanager 可以理解为调试器的抽象,包含所有必要的调试要素。整体结构如下:

    左边是客户端,右边是服务端,内部实现如下:

    客户端实现

    客户端包含了两个个主要功能,一个是和 DAP 服务端进行交互,发送调试命令或处理返回的数据;另一个是将DAP 数据转换后显示到用户界面,并响应界面发送的事件。概括起来就包含业务模块、事件模块、DAP 模块和界面4个部分。

    业务模块

    • 业务模块包含了插件类、调试参数、调试管理类等,其中插件类负责插件加载、初始化、获取上下文等,调试管理类用来组合事件、DAP、界面几个模块。 事件模块

    • 事件模块包含两个子模块,分别是事件发送和事件接收,比如页面跳转事件、添加\移除断点事件等。 DAP 模块

    DAP 模块基于 cppdap 开发,采用层级结构,底层是原始 DAP 协议封装,中间层是针对业务做的进一步封装,简化了向外提供的接口,最上层是对整个调试功能的整合,包括数据缓存、界面元素、命令收发。

    • 界面部分 界面模块包含堆栈界面、变量界面、断点列表、异步对话框等,用于 DAP 的数据展示。
    • 如上图所示,灰色部分为 DAP 客户端的界面呈现。

    服务端实现

    服务端的功能分为两个部分,一个是基于 cppdap 实现命令的收发,另一个是与 gdb 交互,实现调试程序的启动、暂停、退出等一系列动作。

    DAP

    • 和客户端一样,服务端也是基于cppdap实现的通信和协议封装和解析。 调试工具

    • 和调试工具的交互是通过进程调用的方式实现,接收进程输出得到返回信息。如果调试工具本身支持 DAP 协议,则可以直接交互。

    至此,本次的分享就到这儿啦!不知道你对 deepin-IDE 中的调试功能有所了解了吗?

    温馨提示,deepin-IDE 还包含很多有意思的功能,如果大家感兴趣可以积极反馈,后续有机会再进行分享。

    参考文档

    debug-adapter-protocol

    deepin-IDE 使用手册

    内容来源:deepin社区

    内容作者:deepin-mozart、toberyan

    转载请注明出处

    Friday, September 1, 2023

    楔子

      有这么一个说法,每多一个数学公式,读者就减少一半。深度学习想来也无法免俗,毕竟技术文章不免艰涩,而要完全绕过公式讲好深度学习与大模型,以臣妾微薄的实力实在是做不到啊。

      因此,本文先歪歪楼,讲讲深度学习与大模型的历史与八卦,一方面是让大家稍微了解下技术发展的脉络,另一方面也是尝试挽救一下读者的欢心,毕竟历史八卦,人人都爱。

    历史

      说到神经网络的起源,一般都会追溯到沃尔特·皮茨(Walter Pitts)与麦卡洛克(McCulloch),其中皮茨起到了更主要的作用,而且更具传奇性。皮茨(见图1)于1923年出生于美国底特律的铁匠家庭,家庭教育以老爸的拳头为主,而在这种家庭环境里,他主要靠自学学会了拉丁文、希腊文、逻辑和数学。在12岁那年,皮茨看完了罗素与怀特海的大厚本《数学原理》,并向罗素写邮件附上了自己发现的一些问题,罗素不免大吃一惊,还回信邀请皮茨到剑桥大学读他的研究生。由于家庭与年龄原因,皮茨当然不可能成行,不过当三年后,罗素到美国芝加哥讲学的消息传到皮茨耳朵里时,他就离家出走,而且终其一生再也没有回去过。其后皮茨遇到了麦卡洛克,两人在数学、逻辑和神经网络上有着共同的看法,并一起努力,于1943年合作完成了知名论文《A LOGICAL CALCULUS OF THE IDEAS IMMANENT IN NERVOUS ACTIVITY》,在这篇论文中,他们用二进制逻辑门来表示神经元,而且证明了此模型可以实现任何经典逻辑,从而表明了神经网络的通用性,奠定了深度学习的基础,同时也建立了神经科学和计算机科学之间的交叉研究。高中未毕业的皮茨受到了数学家与控制论之父维纳的欣赏,破格进入麻省理工学院攻读博士学位,被引荐给了冯·诺依曼,而上述论文也成为了冯·诺依曼关于计算机架构的著名论文《First Draft of a Report on the EDVAC》所引用的唯一一篇文章。但是在其后,首先是维纳与皮茨等断绝了关系,然后科学家们又发现神经网络并不能解释一切生物感知现象,皮茨深感失望,于是烧掉了自己历经数年写作的博士论文,从此退出了科研界,并死于酗酒相关的病症。

    图1 沃尔特·皮茨

      我们要介绍的下一位是弗兰克·罗森布拉特(Frank Rosenblatt),他是感知机(Perceptron)的提出者(感知机在我们之前的文章“深度学习入门”里介绍过),并且于1958年在实验了50次之后,让IBM 704自行学会了识别打孔卡上的标记是在左侧还是在右侧。他认为,通过感知机可以不依赖人类的训练与控制,就能感知、识别和辨认周边的环境,其代表作是《Principles of Neurodynamics: Perceptrons and the Theory of Brain Mechanisms》。但是,当时罗森布拉特设计的感知机实际上是只有一层的神经网络,能力非常有限,相比之下,现代成熟的神经网络则有数十层,因此同样是人工智能专家的明斯基撰文指出了感知机的局限性,并使得相应的研究进入了低潮。直到在2004年,大家重新看到了神经网络的潜力,IEEE协会为此也专门设立了罗森布拉特奖。

      在神经网络漫长的寒冬期,研究者相对较少,其中就包括了于1974年在其博士论文中提出了反向传播(backpropagation)的Werbos(并由于此贡献获得了IEEE罗森布拉特奖),还有于上个世纪八十年代提出了Hopfield 神经网络的Hopfield。

      下面隆重登场的是深度学习之父,大名鼎鼎的杰弗里·辛顿(Geoffrey Hinton)。辛顿1986年发表的论文《Learning Representations by Back-Propagating Errors》,给出了通过反向传播学习表征的算法,于2006年其推出了深度学习(Deep Learning)的概念,为深度学习与大模型的大潮解开了序幕。

    图2 杰弗里·辛顿

      另一位深度学习的大佬杨立昆(Yann LeCun)在辛顿麾下求学后就职于贝尔实验室,并在1989~1993年间发明了卷积神经网络(CNN),可以用来解决手写数字识别(如MNIST)的问题。当时可没有GPU,那时CPU的性能也相当低下。

      2012年是深度学习重要的一年。在这一年,辛顿和他的两个学生Alex Krizhevsky,以及Ilya Sutskeverz共同发布了AlexNet这个多层神经网络。这个神经网络用到了诸多的新技术,包括使用了ReLU作为激活函数,使用了CUDA利用英伟达的GPU来进行神经网络的计算,使用了dropout作为神经网络的一种优化方法等。AlexNet在知名的图像识别分类比赛ImageNet中一鸣惊人,它不仅获得了比赛的冠军,而且其错误率达到了16.4%,比当年亚军的26.2%低了将近10%,比2011年冠军的25.8%低了超过9%,几乎可以认为是降维打击。自此,深度学习名声大噪,大量研究人员都转向深度学习尝试完成计算机视觉等相关的人工智能任务。

      下面深度学习的发展就顺畅了很多,2014年Ian Goodfellow推出了GAN,通过生成式对抗网络能生成逼真的图像甚至视频;2016年 DeepMind 推出了AlphaGo,其水平很快就远远超过了人类围棋冠军;同样在 2016年,何恺明等研究者推出了ResNet,它成为了包括大语言模型在内的各神经网络的通用技术,对应论文引用截止2023年上半年已经超过了17万,成为了深度学习领域引用最高的文章。

      2017年是大模型关键技术transformer的诞生年,它是在《Attention is All You Need》这篇论文中被提出的,其后就成为包括BERT、GPT、T5等大语言模型使用的框架,而且跨界到了计算机视觉领域,形成了ViT等新的研究方向,几乎成了一统江湖的标准模型。

      2018年是深度学习三巨头辛顿、杨立昆与约书亚·本吉奥(Yoshua Bengio)的收获之年,他们因为在深度学习方面的诸多成就与影响力获得了ACM图灵奖,这项计算机科学领域的最高奖。在接下来的几年,深度学习的各项研究成果仍然难以大规模落地,诸多深度学习相关的公司持续烧钱,很多人认为这一波人工智能的热潮马上又要过去了。

      让我们快进到2022年,上半年以stable diffusion为代表的图像生成模型风靡一时,而到了下半年的十一月底,以ChatGPT为代表的大语言模型横空出世,其知识广度、推理能力与多轮对话能力使得它成为了历史上最快达到一亿用户的产品,重新点燃了人工智能产业化的火炬,并使得业界的诸多大佬认定它是划时代的产品,其影响深远,延续至今。

    图3 DNNResearch团队

      顺便说一句,GPT系列大模型的出品者是OpenAI,而OpenAI的首席科学家就是之前提到过的Ilya Sutskeverz。在2012年AlexNet一飞冲天后,多个大厂邀请辛顿等三人加入,于是辛顿团队成立了一个名为DNNResearch的公司,公司仅有他们叁,公司唯一的目的就是被大厂整体收购。竞拍的公司包括谷歌、百度、微软与DeepMind,随着竞拍价格的逐步走高,微软与DeepMind首先被淘汰出局,谷歌与百度均出价到了4400万美元。辛顿当晚暂停了竞拍,第二天早上便决定公司被谷歌收购。Ilya Sutskeverz在其后做出了诸多贡献,包括首创seq2seq(大语言模型的前身),参与深度学习框架Tensorflow与AlphaGo的开发等,最终于2015年加入OpenAI,成为了GPT等系列产品的核心技术人物。

    八卦

      关于深度学习之父辛顿,还有很多不少八卦,其家族还与中国有着不少联系。

      从头说起,George Everest是十九世纪英国的大地测量工作者,其主要的工作地域都在印度,他和其前任测量了从喜马拉雅山到印度次大陆最南端科莫林角11.5度的经向弧,由于这些贡献,珠穆朗玛峰(Mount Everest)以他的名字命名。George Everest有一个侄女婿名为乔治·布尔(George Boole),也就是著名的布尔代数的发明者,现代计算机所使用的逻辑与、或、非等运算均来自于他。

      乔治·布尔的小女儿艾捷尔·丽莲·伏尼契(Ethel Lilian Voynich)则是知名的《牛虻》的作者,这本书描写的是19世纪意大利爱国者反对奥地利统治者的斗争,是风靡一时的革命书籍,在苏联和中国都拥有广大读者。

      乔治·布尔的大女儿玛丽(Mary)则嫁给了一个姓辛顿的数学家,他们有一个孙子威廉·辛顿(William Hinton),以及孙女琼·辛顿(Joan Hinton),不过在中国,他们一般被称为韩丁与寒春。寒春是芝加哥大学核子物理研究所的研究生,也是曼哈顿计划中少数的女科学家之一,在洛斯阿拉莫斯(Los Alamos)武器试验室做费米的助手,其后反对核战争,并于1948年来到中国,进入延安,投身中国革命,并与阳早(Erwin Engst)结婚,成为了奶牛专家。现在在北京还留有他们的工作成果,那就是北京市昌平区沙河大学城农机院的学农基地,寒春与阳早均为农机院学农基地的创办人,时至今日,每年仍然有大量的北京中学生来到学农基地参加学农实习(笔者的小孩有幸成为其中的一员)。寒春和阳早把大部分生命都奉献给了中国的事业,并最终在中国去世。2004年8月,中国开始实施“绿卡”制度,寒春成为了第一个获得中国“绿卡”的外国人。

    图4 阳早与寒春

      玛丽的另一支当然就是深度学习之父杰弗里·辛顿了,他是玛丽的曾孙,因此比寒春、韩丁晚一辈。当然,他们之间并没有直接联系。

      深度学习的历史与八卦到此告一段落。下面,我们又将步入正轨,重新来讲讲产品与技术了,咱们下次再见。

    Thursday, August 24, 2023

    应用大模型

      AIGC 是基于大模型的,而大模型的基础是深度学习。上一篇文章对深度学习进行了初步介绍,首先是深度学习的神经元起源,引发了基于线性函数的模拟,又因为线性函数无法习得逻辑异或,因此引入了非线性的激活函数,再通过三层神经网络给出了MNIST手写数字识别的模型,接着又介绍了神经网络是如何通过数据与反向传播来学习与调整参数的,最后给出了神经网络的分层结构。

      大模型的直观应用当然首先体现在包括ChatGPT、文心一言、讯飞星火等问答型产品的使用上,另一方面也体现在编程上,在此先给出大模型的编程应用。以下使用的模型、库与样例均来自于Hugging Face。

      图1给出了基于大模型的英中翻译代码与运行结果。从图1中可以看到,真实的翻译代码只有14、15两行,其逻辑是使用了Helsinki-NLP的opus-mt-en-zh模型,其中mt代表机器翻译(machine translation)、en和zh分别表示英文和中文。从图1中同样可以看到,翻译结果相对还是比较准确的。

    图1 基于大模型的英翻中

      图2给出了基于大模型的文本情感分析的代码与运行结果。从图2中可以看到,实际有用的代码也仅需14、16两行,而且这次没有指定具体模型,只给出了需要text-classification这种模型。代码运行结果是认为文本情绪是负面的(NEGATIVE),准确度大概是90.1546%,这显然是符合实际的,因为文本是对商家发错货的抱怨。

    图2 基于大模型的文本情感分析

      图3给出了基于大模型的问答。这次的代码稍多一点,但实际的代码也只有三行。第14行给出了需要一个问答(question-answering)的大模型,但是没有指定大模型的名字,第15行是提问的字符串“What does the customer want”,即用户到底想要什么。第16行则使用上述文本作为上下文,提问字符串作为问题,传给问答大模型获取答案。从运行结果看来,答案还是蛮靠谱的。

    图3 基于大模型的问答

      当然,基于大模型的程序还有很多,但是从上面三个例子已经可以看出,基于大模型可以写出简短而强大的自然语言处理的程序,下面让我们走进大模型,看看它究竟是如何做到这一点的。

    走进大模型

      大模型在自然语言处理领域里大放异彩,因此首先需要了解自然语言的特点。

      自然语言的显著特点(也是难点),那就是词与词之间有着广泛的关联。比如下面两句英文:

    • Go to the bank to get some money.
    • Go to the bank to get some water.

      只有看到每一句的最后一个词,才能分辨出 bank 到底是银行还是堤坝。再比如下面这两句中文:

    • 今天太冷了,能穿多少穿多少。
    • 今天太热了,能穿多少穿多少。

      整句话唯一不同的就是冷与热这两个字,但也就是这句话的一字之差,就导致了整句话的意义完全不同了。

      因此,自然语言处理的关键点就在于如何能准确地判断词与词之间的关系,如果能准确地知道所有词之间的关系,那即使缺了一个词,也能根据关系推出缺的词应该是什么词。当下处理这一问题的主流技术是transformer,这个词不好翻译,主要因为它和变形金刚的英文一模一样。transformer的核心概念是注意力(attention),即每个词到底在注意其它的哪个词,或者说哪些词之间有什么关系。

      注意力具体由以下关键概念组成:

    • 每个词(实际上是词元,token)均有对应的 q/query(查询)、k/key(键值)与 v/value(值)这三个矩阵变量
    • q[i]用来查询本词(i)与其它词(j)之间的注意力
    • k[j]是词j回应查询的键值,具体是q[i]与k[j]相乘后缩放,接着用softmax激活函数处理,再乘以v[j],这就得到了词i针对词j的注意力att[i,j]。
    • 词i的对应输出为计算得到的注意力之和,即 y[i] = att[i,1] + att[i,2] + … + att[i,n]
    • 注意力可以有多个(multi-head),每个注意力可以关注不同的方向,例如有的注意力关注的是词与词之间的意义,有的关注的是押韵,等等

      图4给出了一个句子中各个词注意力的计算过程。

    图4 注意力计算过程

      句子是“小明、小刚、小红是小强的朋友,小明是…”,当前的词是“小红”,序号是3,可以看到小红的q值与每个词的k值相乘之后再用softmax处理(缩放操作在这里省略了),接着再与每个词的q值相乘,最后相加即可得到序号为3的输出。

      那么这些k、q、v 等的值如何确定呢?当然是通过上一篇文章里提到的反向传播进行学习的,那反向传播学习自然语言的正确输出是什么呢?在大语言模型中,其训练手段是使用大量的高质量语料,将词语按序逐批输入大模型,以原句子中的下一个词或者特意被空缺出来的词为正确输出来学习的。例如在上面的句子中,大模型在输入了“小明、小刚、小红是小强的”之后,应该能计算输出“朋友”这个词,如果输出错误,则通过反向传播调整各个参数。也就是说,大模型就是根据一个词之前的词或者周围的词是什么来计算出这个词的,这就是大模型的理论基础和学习方法。

      注意力虽然是大模型的核心概念,但除此之外,大模型还用到了其他技术,它们包括:

    • 第一步需要将词转为数值向量(vector)的嵌入层(embedding),这也是现在很火的向量数据库的那个向量,向量数据库就是用来查询哪些文本向量比较相似,从而提取出对应的文字材料的
    • 给词向量加上含位置信息的位置编码,以分辨出词的先后顺序,显然“我爱你”与“你爱我”的意义截然不同,因此词序是很重要的,而注意力本身并没有用到词序
    • 在注意力层之后的残差处理与归一化处理,技术原理并不难,感兴趣的读者可以看参考资料

      现在我们已经对大模型的主要技术原理有了粗浅的了解,下面来看看深度学习与大模型的特点。

    大模型的特点

      从对深度学习以及大模型的初步应用与原理了解,其实我们已经可以得出一些结论。

      首先,深度学习的算法模型是神经网络及其节点上的参数/权重构成的,深度学习是端到端的机器学习,它不用由人类专家预先设计特征算法,例如不用苦思冥想怎么设计一个提取图像中线条的算法,只需要用大量的高质量标注数据对其进行投喂,就有可能最终训练出质量不错的神经网络(中的各个参数)来。

      其次,它是一个黑盒子,因为没有特征设计,因此这么多数值构成的神经网络,它为什么能达到最终的目标,为什么某个参数就必须是最后训练出来的那个数值,为什么有的时候识别会失败或者生成会出错,是否可以手工修改某些特定的参数使得其成功率上升,同时又不会导致原来成功的事例失败,这些问题在现阶段都是难以回答的,这就是深度学习的可解释性问题。

      在ChatGPT之前,大模型其实也在发展,但是ChatGPT表现出来的多领域通用性和流畅性与之前的产品相比有明显的超越,所以造成了轰动。其后的GPT-4表现更为优异,在《Sparks of Artificial General Intelligence:Early Experiments with GPT-4》这篇论文里,微软团队给出了这样的一个示例:

    提问:Can you write a proof that there are infinitely many primes, with every line that rhymes?
    GPT4:Yes, I think I can,
    Though it might take a clever plan.
    I’ll start by noting Euclid’s proof,
    Which shows that primes aren’t just aloof.
    Assume we have a finite list,
    Of primes, and that none have been missed.
    Multiply them all together,
    And add one, just to be clever.
    The result will be a number,
    That has no prime factors, oh wonder!
    But every number has a prime,
    That divides it, it’s just a matter of time.
    So we’ve found a contradiction,
    And our finite list needs eviction.
    There must be infinitely many primes,
    And that’s the end of my rhyming lines.
    

      提问者让GPT-4写一首诗,来证明素数有无限多个,然后GPT-4很快就完成了这首诗,每两行押韵,而且用欧几里得的经典方法给出了证明。当然,在论文中还有很多让人印象深刻的例子,但是上面的例子已能说明GPT-4确实有了很强的能力。

      GPT中的G表示生成(Generative),这表示它主要的工作方式是生成内容,内容在这里主要是自然语言文本。按照OpenAI首席执行官Sam Altman的说法,The most important difference between ChatGPT and GPT-4, is that the GPT-4 predicts the next word with greater accuracy,即 ChatGPT与GPT-4最大的差异就在于GPT-4在预测下一个词的准确度比ChatGPT更高。

      GPT中的P表示预训练(Pretrain),即首先用大量语料训练出基础模型(foundation model),然后再用下游任务相关的语料进行精调(FT,即finetuning)。这些下游任务可能是文本分类、翻译、对话等等,这样就可以不用单独为某个特定任务从头训练了。我们可以把中学教育和通识教育看作是预训练,它为大学最终的专业选择,以及以后更细的工作分工打下了坚实的基础。反过来看,在小学年龄阶段没有上学可能会对以后的择业带来很大的限制,这也可以看成是大脑在应预训练的阶段没有进行有效的预训练导致的问题。

      GPT中的T表示transformer,这个已经在上文中介绍了。

      一般认为,GPT-4有更强能力的原因在于:

    • 它提供了大量的高质量数据,原始数据有45T,清洗后的语料是570G,清洗比例接近1%,这是之前几乎没有团队做到的
    • 数据中混合了大量的代码,原始数据中有830G代码,这一般被认为是推理能力提升的关键点之一,当然另一方面也大大提升了它的代码能力
    • 展开了大量不同种类的下游任务,如生成、问答、脑暴、闲聊、摘要、分类、提取等等,以上两点也属于多样化工作,它为GPT4的通用性打下了基础
    • 使用了基于人工反馈的增强学习(RLHF)方法,召集了40个众包团队,撰写了数十万的提示数据以对齐主流价值观

      一般认为,大模型的表现之所以如此智能,但是之前的小模型神经网络却那么智障,其原因可能在于涌现(emergence)。涌现可以简单认为是单个个体微观上简单的行为,在宏观上大量复合呈现出难以预料的规律。比如每只蚂蚁其行为其实是挺简单的,但是一群蚂蚁在一起,就可以表现出复杂的规律。又如每个神经元的行为都很简单,但是这么多神经元聚集在一起,就形成了聪明的人类大脑,这也算是一种涌现。

      涌现最直观的例子可能就是康威的生命游戏(Conwey’s Life Game)了,这个游戏是在一个网格平面(类似围棋棋盘)上发生的,每个个里要么有一个存活的细胞,(用黑色格表示),要么就是一个死亡的细胞(用白色格表示),其规则也很简单,只有以下四条:

    • 当前细胞存活时,当周围存活细胞<2时,该细胞死亡(模拟生命数量稀少)
    • 当前细胞存活时,当周围有2个或3个存活细胞时,该细胞保持存活
    • 当前细胞存活时,当周围存活细胞>3时,该细胞死亡(模拟生命数量拥挤)
    • 当前细胞死亡时,当周围存活细胞=3时,该细胞复活(模拟繁殖)

      那么图5里的四个样式就表示绝对静止的细胞群体。

    图5 康威生命游戏中绝对静止的细胞群体

      对每个样式进行分析很快就会知道为什么它们会绝对静止。以第二个样式为例,其每个存活细胞周围都刚好有两个存活细胞,按照规则2,它们都应该保持存活。而任何一个死亡细胞周围都没有四个存活细胞,因此此样式将永远不变。

      图6给出了震荡循环的细胞群体。

    图6 康威生命游戏中震荡循环的细胞群体

      按照康威生命游戏的规则,可以发现图6中的两个样式会演变几步之后又变成当前的样式。以第二个样式为例,其一共有左中右三个存活细胞。左侧与右侧的存活细胞附近只有一个存活细胞,因此按照规则1,会在下一轮死亡。中间的存活细胞附近有两个存活细胞,因此按照规则2,保持存活。同时又可以发现,中间的存活细胞上侧和下侧的死亡细胞由于其附近有三个存活细胞,因此根据规则4,在下一轮它们将复活。以此类推,样式会由横三转为纵三,又转回横三,永远震荡循环。

      康威生命游戏有着远超过上述样式的复杂度,在宏观上甚至可以看到游走、巡回、扩张、凋零等多种细胞社群的样式,因此四条简单的微观规则就衍生出了让人事先难以预料的宏观样式上的复杂度,是涌现的一个生动形象的例子。

      以ChatGPT与GPT-4为代表的大模型由于其使用了自然语言对话而引发了轰动,让普通人都能直观感受到大模型的魅力,但同时它作为一个基础设施,也提出了一个难题,就是它的编程接口是基于自然语言的,所以需要做所谓的提示工程(prompt engineering)。所谓提示工程,指的就是想让大模型好好干活,那就需要自己好好琢磨怎么和大模型好好说话。俗话说见人下菜,或者说见人说人话,见鬼说鬼话,那见了ChatGPT,当然就得说ChatGPT话了,不然它就没法理解问题,自然也没法给出好的回答了。Linux圈子里有Linus大佬的一句名言:“talk is cheap, show me your code”,中文翻译也很传神:“废话少说,放码过来”,俗一点的话那就是“少哔哔,秀代码”,但是自打GPT横空出世,以后可能就是“code is cheap, show me your talk”了,毕竟,给GPT一个提示,它可以还你百行代码。

      不过神经网络毕竟是一种信息压缩,或说是一种函数拟合,因此中间肯定会有信息损失,或说是自己瞎想的填补空白,那就避免不了GPT一本正经的胡说八道,也就是所谓的幻觉(hallucination)了。幻觉是当前大模型应用的主要障碍之一,一般认为,大模型近期的发展将沿着消减幻觉、工具集成(即能使用外部工具)、多模态(即除了文本以外,也能理解和生成图形、语音、视频等内容)、垂直领域、类脑智能、具身(embodied)智能等方向发展。

    Monday, August 21, 2023

    背景

      从去年底以来,AIGC 炙手可热,多个业界大佬都认为 AIGC 会给整个产业带来一场革命,甚至所有的软件都会用 AI 重写。从历史上来看,人机交互方式的变革往往会将操作系统带入下一个世代,著名的例子如从命令行界面的 DOS 到键鼠图形界面的 Windows,以及带来触控界面的 iPhone,领创者都成为了世界顶级企业,带动了整个生态的发展。

      从技术上来看,AIGC 是基于大模型的,而大模型的基础是深度学习,因此,为了在产品上结合 AIGC,首先从技术上首先需要对深度学习进行有深度的学习。

      对深度学习与大模型的探索将由一系列文章组成,本文是系列里的第一篇,主要关注的是深度学习的技术入门探索。

    从神经元开始

      回溯历史,深度学习起始于向人类的大脑学习如何学习。人类大脑皮质的思维活动就是通过大量中间神经元的极其复杂的反射活动,因此不妨先看看神经元的工作机制。

    图1 神经元结构

      图1给出了神经元的大体结构,左边是神经元的主体,其输入是左侧的多个树突,其输出是右侧的一个轴突。只有当输入树突的信号足够强烈的时候,输出轴突上才会有信号产生。受此启发,就可以设计一个最简单的有两个输入x1与x2,以及一个输出y的线性函数来模拟单个神经元,引入阈值θ,当 w1x1 + w2x2 ≥ θ时,y为1(表示有信号),否则y为0(表示无信号)。其中w1与w2分别是x1与x2的参数或权重(weight)。

      有了这个函数,下面来看看它究竟能做什么。按照逻辑主义的设想,数学可以通过逻辑推衍出来,那么不妨看看,上面的函数是否可以表征出基本逻辑运算,如与、或、异或等,在这里x1、x2与y的取值都只能是0或1。

      对于逻辑与来说,只有当x1与x2都是1的时候,y才是1,否则y是0,容易尝试得到一组可能的w1、w2与θ,分别是0.5、0.5与0.7,如图2所示。

    图2 逻辑与的线性函数图

      图2中横轴为x1,纵轴为x2,从图2中可以看到,(1, 1) 点为实心圆,表示y为1,在(0, 0)、(0, 1)与(1, 0)都是空心圆,表示y为0,中间的虚线表示w1x1 + w2x2 = θ这条直线,只要这条直线能将(1, 1)点与其它点划分到不同区域,则显然就可以找到至少一组w1、w2与θ满足条件。基于同样的分析,容易知道逻辑或也可以找到对应的w1、w2与θ。但是对于逻辑异或来说,问题就严重了,显然无法找到满足条件的w1、w2与θ,如图3所示。

    图3 逻辑异或的函数图

      逻辑异或是当x1与x2中一个为0,另一个为1时y才为1,否则y为0,因此在图3中,点(0,1)与点(1,0)为实心圆,而(0, 0)与(1, 1)为空心圆,显然是无法找到一条直线将两个实心圆与两个空心圆划分在两个不同区域的。因此,上述最朴素的线性神经元函数无法表示逻辑异或,也就意味着有大量的运算无法通过上述线性神经元函数来进行。

    引入激活函数

      是否能改造上述函数,让它能支持所有运算,从而能承担学习的任务呢?至少,人脑肯定是能学会异或的。现在看来,主要是因为原始的神经元函数太线性导致的这个问题。因此,在深度学习中,就引入了非线性的激活函数(activation function),如图4所示。

    图4 引入激活函数

      在图4中,首先原函数被修改成了支持多个输入和多个输出的线性变换函数,这样就能处理更多种类的问题了。因为有了多个输入x1、x2…xm与多个输出h1、h2…hn,因此权重的下标也带有两个数字,以表示每个权重的作用,例如 w12 是输入x2与输出h1间的权重。还有一个特殊的权重bi,它被称为偏置(bias),是一个待确定的常数项。这样,h就等于相应的x与w相乘后再加上b。例如,hi = xiwi1 + x2wi2 + … + xmwim + bi。   经过线性变换后得到的输出h1、h2…hn只是中间过程的输出,在之后,还需要加入一个非线性的激活函数的处理,以得到最终的输出y1~yn,如图4所示。

      在具体激活函数的选择上,比较常见的有 softmax、sigmoid 与 relu 等。其中 softmax 函数是多分类问题最常用的输出激活函数(多分类问题指的是一个问题有多个确定个数的可能答案,例如是/否问题是二分类问题,而分辨一个手写阿拉伯数字是哪个数就是一个十分类问题,因为可能答案有0~9一共十个),softmax也是包括ChatGPT在内的大模型使用的输出函数。   使用了激活函数以后,神经网络就可以学习到所有函数了。下面来看一个经典的神经网络的例子,手写数字识别问题,或MNIST问题。MNIST涉及的手写数字在网上是公开的,如图5所示。程序员们可以先想想,如果自己来写一个程序识别手写数字会怎么写。可以识别手写数字的(一个)神经网络的结构如图6所示。

    图5 MNIST手写数字样例

    图6 能识别手写数字的神经网络

      可以看到图6的神经网络一共用到了三个线性变换,并使用了两个sigmoid 激活函数,以及最后的softmax激活函数,因此可以说这个神经网络是三层的。神经网络的输入(x1~x784)是一个长度为784的数组,其实就是一个28x28=784的手写数字的黑白图像。神经网络的输出(y1~y10)分别代表了0~9的阿拉伯数字,这是一个典型的十分类问题,因此使用softmax也是非常自然的。

      图6中的神经网络一共有(784x50+50) + (50x100+100) + (100x10+10) = 45360个参数,对比ChatGPT上千亿个参数,这显然是一个微模型,但是它的识别能力却可以达到92.53%,也就是说一万个手写数字,它能正确识别出9253个来。   那问题就来了,这45360个参数是怎么来的呢?肯定不能是随便什么 45360 个数都能带来这么高的识别率的,要解决这个问题,就需要看看神经网络是怎么学习的了。

    神经网络的学习

      在上面已经看到,神经网络里有大量的参数。在最开始,这些参数会被随机分配一些数字(当然如何随机分配也有讲究的,简洁起见,此处先不提),此外也需要准备大量的数据,这些数据一般是多个输入输出的对(x, t)。例如在上面的手写数字识别问题中,输入x就是一个28x28的手写数字图像,输出t就是这个图像对应的0~9中的一个数字。   这些数据会被分成训练集与测试集。训练集中的数据用来训练神经网络,让神经网络中的参数最终达到正确的值。测试集中的数据用来测试训练后的神经网络,对比看训练后的神经网络在新的数据下得到的结果是否正确。

      神经网络的训练过程可以大体分为下面几步:

    • 对训练集中的输入输出对(x, t)进行如下处理

    • 将x输入到神经网络中,计算得到y

    • 将y与正确的输出t进行运算得到损失L,损失的计算函数一般是均方差或交叉熵,前者针对的是回归问题(连续函数拟合),后者针对的是分类问题

    • 根据L调整神经网络的参数,调整的方向是减少L,调整的方法是下面要讲的反向传播

      图7给出了神经网络训练的过程。

      图7 神经网络训练过程

        一旦训练完毕,使用的时候就不需要正确输出t,也不需要计算损失L和调整神经网络的参数了,这个过程被称为推理(inference),如图8所示。

      图8 神经网络推理过程

        顺便说一句,图中的深度神经网络与神经网络结构是一样的,但是层数较多,因此被称为深度神经网络。

        下面,再来看看神经网络究竟是怎样通过损失L来调整网络参数的。最简单,也是最直观的方法就是将每个参数都稍微调大或者调小一点,看L会如何变化,如果L变小,则保持此参数的调整,如果L变大,则将此参数反过来调整。以上即正向调整法,思路清晰,操作方法简单,但是计算量极大,因为每调整一个参数就要重新计算一遍y与L。

        另一种方法就是现在主流的反向传播(BP,backpropagation)法,此方法类似系统发生故障时的根因分析,首先分析最后一层的参数是怎样影响到L的,然后分析倒数第二层的参数是如何影响到最后一层的输入的,如此类推。在数学上,其实就是计算L对某个特定参数w的(偏)导数,因为导数就代表了w的变化会导致L如何变化。根据链式求导法则,L对w的导数等于L对中间变量h的导数乘以h对w的导数,前者相当于计算最后一层参数的导数,后者相当于计算倒数第二层参数的导数,两者相乘即为L对导数第二层参数的导数。

        下面主要通过求导来展示反向传播,如果希望更直观一点,可以阅读计算图相关的资料。假设真实函数是y=2x+1,则待求函数为wx+b(当然w与b的真实值应该是2与1)。下面通过一组数据(训练集)来通过反向传播逐步计算更新w与b,看看它们否会逐渐逼近2与1。

        由于这是一个回归问题,因此使用均方差(y-t)< sup>2< /sup>/2作为损失L的函数,显然L对y的导数是y-t,参数更新使用经典的梯度下降法(SGD),即参数新值=参数旧值 - 学习率x(L对参数的导数),梯度下降有一个粗糙但是直观的理解,那就是学习应该向着导数(梯度)相反(下降)的方向走,在这里学习率这个参数设为0.01。

        首先,将w与b随机化为0.5与0.6。

        假设第一个训练对为(0, 1),则 y = wx + b = 0.5·0 + 0.6 = 0.6,L对w的导数=L对y的导数乘以y对w的导数=(y-t)·x=(0.6-1)·0=0,L对b的导数=L对y的导数乘以y对b的导数=(y-t)·1=-0.4。则w的新值为w-0.01·0=0.5,b的新值为b-0.01·(-0.4)=0.604,显然新的w与b比原来的更接近(2, 1)。

        若第二个训练对为(1, 2.9)(本来应为1与3,但是增加了一点误差干扰),可以以同样的方法得到新的w为0.51796,而新的b为0.62196,显然比上一对w与b又接近了2与1一点。

        实际上,若继续增加2x+1附近的数据,可以发现到了十几对训练数对之后,w与b即可相当接近2与1了。

        以上例子是为了直观感受反向传播的计算而给出的,实际上这种线性函数的回归可以通过数据集基于矩阵一次性算出来,而且训练本身也要考虑收敛的问题,因此实际的深度学习会更复杂一些,但是原理是类似的。

        总地来说,深度神经网络是由多个层组成的,每一层均有前向(forward)推理的函数,用来从输入计算得到输出,这个过程即为推理。每一层也有反向(backward)传播的函数,用来从后一层传来的导数计算得到本层向前一层传递的导数,并同时更新本层的参数。如果是训练,则需要在最后一层再加上一个输入为t与y的损失层,输出为L,如图9所示。

      图9 多层神经网络结构

        通过以上几乎标准化的神经网络层,深度学习的研究者就可以像搭积木一样对多个层进行排列组合,得到多种多样的深度神经网络,并首先通过反向传播训练出神经网络的参数,继而使用神经网络进行推理应用了。