分类目录归档:编程

谨慎的乐观的看 Modular 提出的 Mojo 语言

有了 faster-cpython 的前车之鉴,对 Mojo 谨慎的乐观。

宣称将成为 Python 的超集是 Mojo 相比 Codon 等项目最吸引人的一点。Codon类项目虽然宣称是Python的编译器,但实际上砍掉了 Python 所有的动态特性,几乎所有的 Python 库都无法正常使用。对我而言,如果用不了 Python 生态,那这和一个全新的语言又有什么区别。

如果 Mojo 真成为Python的超集,单就比 CPython 性能高出一大截的 Python 编译器就足够吸引人。但细看下来,“成为 Python 超集”可能只是一个美好的愿望,达成的可能性非常小。现在 Mojo 的完成度还非常低,除了支持 Python 风格的语法外,Python还差很远(连 Class 都不支持)。现在版本的 Mojo 支持导入 Python 模块,但导入的 Python 模块是以 Python 对象的方式运行。换句话说就是塞了个 CPython 解释器到 Mojo 里用来执行 Python 代码(是不是立马不高大上了)。

除去 Mojo 吹牛的部分(比如那个比 Python 快 3500 倍),对 Mojo 还是有所期待。Mojo 的创始人有着牛逼哄哄的履历(LLVM & Swift 的作者)。虽然不能完全兼容 Python ,但承诺后期会提供相关的迁移工具。如果 Mojo 的性能和开发体验确实不错,Python库的迁移成本不高,还是会有不少人会自发的将 Python 生态迁移到 Mojo 到。

注:

一个AI公司需要多少人

Stable DiffusionChatGPT 的大火,让沉寂已久的AI世界再次翻红。又开始有人在问,在AI越来越成熟的今天,程序员是否有必要去学算法,投身AI行业。在我看来对程序员而言AI带来的机会更多是如何利用AI带来的能力而不是去创造一个AI。

曾在AI公司待过一段时间,这段经历给我最大的感觉是: AI 行业是一个资本密集型产业,对普通程序员来说没太多的机会。AI的发展对普通程序员带来的最大变化是出现来一批很好用的 API ,可以实现一些以前实现不了的想法。另外就是如何利用新出现的一批AI工具提高自己的工作效率。

为什么不建议普通程序员进入AI行业

简单来说AI行业需要的专业算法人员非常少,且门槛非常高。非算法相关的人员,相对而言门槛又太低。

  1. 常规应用,通用模型已经很成熟了。对于 99.9%人根本不可能构建出一个比开源模型更好的模型。
  2. 模型的性能优化是个体力活,而且随着技术的发展,以后可能不需要手动的性能优化。
  3. 模型的调优需要大量的数据和硬件。数据清洗是和 AI 没关系的纯体力活。参数调优又需要大量的硬件,¥成本普通人(公司)根本承担不起(据说 ChatGPT 训练一次的成本就高达 1 千万美元)。

一个AI公司需要多少人

前面说到AI公司需要的人少,但具体少到什么程度可能会超乎很多人的想象。

OpenAI

OpenAIChatGPT 的母公司),仅2022年,就花费了约5.44亿美元。而与之对应的是”今年1月,OpenAI创始人透露公司员工人数为375人“。关键是这375人里包含了行政等支持人员及数据收集整理人员。

Midjourney

相比 OpenAI ,AI绘图领域的巨头 Midjourney 在人力方面更是做到了极致。Midjourney 总共只有11人:1创始人,1财务,1法务,8研发(其中4人为本科实习生)。

参考

ChatGPT和它的平替们

ChatGPTOpenAI 发布的对话式大型语言模型。它可以帮你写代码,翻译,论色文章等。我现在写英文写完后要都用Bing修正一遍语法错误(发现自己写的句子几乎全有语法错误)。只是在ChatGPT价格不算便宜,且国内使用困难重重。好在还有不少 ChatGPT 的平替可以使用。不过平替总归还是平替,和 ChatGPT 比起来还是有不少差距。

平台国外手机号,付费需申请试用国内直连中文支持备注
ChatGPT付费使用,功能最全,效果最好。
Bing由OpenAI提供技术。
效果最接近 ChatGPT 的最佳平替。
回答里有引用来源。
不会写代码。
Bard谷歌出品。
不会中文,效果不如 Bing 。
文心一言百度出品。
侧重中文,智能程度一般。
应当集成了 Stable Diffusion ,可以画画。
PoeQuora出品的AI 产品。提供多个AI 机器人聚合。目前可以通过Poe免费使用ChatGPT。

GitHub Copilot 平替

GitHub Copilot 可以算是编程界的 ChatGPT ,可根据上下文自动对程序进行补全。刚推出时可以免费试用,后转为订阅制。

平台免费备注
GitHub Copilot最早推出,效果最佳。
Codeium可用,但效果一般。
据称永久免费。
CodeWhisperer亚马逊出品。
注册账号需要绑定信用卡。
效果和 Codeium 差不多。

使用Python做嵌入式开发

出于性能的考虑,传统的嵌入式开发都以C、C++为主。如今嵌入式设备的性能早已今非昔比,开发工具的选择方面也有了更大的自由度。对于非性能敏感的业务,Go、Python等开发语言引入的开发速度提升还是非常诱人。Python有着丰富的开发资源,在系统资源足够的情况下,Python在嵌入式环境下有着不错的开发体验。

性能

同Python高效的开发速度相对应的是Python的运行速度非常的慢,即使在脚本语言里Python也是最慢的一档。如果你的程序需要高性能,Python显然是不合适的。即使不需要高性能,也需要特别注意以保证用户体验。

使用的库尽量精简。在PC下Python的启动速度不会有明显的感觉,但在嵌入式设备下,用到的库多了后,第一个明显的感觉就是启动时间变长。如果用到的库多,启动时间甚至会超过10秒。嵌入式环境下引入一个新库需要更为谨慎,平衡好开发体验及性能影响。

程序打包(应用分发)

Python在跨平台方面做的非常优秀,大多情况下可以不需要嵌入式设备,直接本地开发调试。但程序发布的时候还需要针对应用平台就行打包。

pex会把所有的依赖和你自己的代码打包成一个.pex为后缀的可执行文件。在运行环境下直接执行该文件即可。由于开发环境的构架和运行环境的架构不一致,可以通过Docker容器就行程序的pex打包。

代码保护

对于商业项目,必要的代码保护还是有一定的必要。代码的保护可以选择下面几种方式。

  1. 编译成pyc
    • 使用命令 `python3 -m compileall -b ./;find ./ -name “*.py” -delete` 将代码编译成pyc,并删除py文件。
    • 该做法可以提供最低限度的代码保护。pyc还是可以较容易的反编译成py文件。
  2. 使用代码加密(混淆)工具对源代码进行加密。
    • 开源的代码加密工具都缺乏维护,很久未更新。如果有代码加密需求,建议使用商业工具。pyarmor
  3. 使用 CythonNuitka 等工具将代码编译成二进制文件。
    • 相比 Cython,Nuitka的使用要简单很多,建议优先使用Nuitka。需要注意的是使用 Nuitka 后内存占用率会比直接用Python解释器高大概 1/3 。
    • Nuitka的编译也可在Docker容器中进行。

Python内存泄漏原因及问题排查

Python 会自动回收内存,一般情况下不用关心内存的申请和释放问题。事实上我也一直没怎么关心过Python的内存管理问题,直到我用了 Python Prompt Toolkit 。这是一个 Python 的CLI组件库,使用简单,效果很好。只是性能用点差,另外就是它居然有内存泄漏。

内存问题产生原因

Python里内存管理主要基于引用计数实现,另外会辅以全图遍历以解决循环引用问题。一般内存问题都是对象被全局变量直接或间接持有导致。出现内存泄漏后关键是找到问题对象到底被谁给持有了。

确认内存泄漏的对象

如果一个程序内存一直异常增长,那多半是存在内存泄漏。接下来就是定位问题了。Python内存分析的工具和手段主要有下面几个:

  1. objgraph 可用现实对象的增长情况。还可以根据对象的引用关系生成图像。
    • 可以根据对象生成引用关系树以及被引用关系树。第一感觉功能很强,实际用下来效果一般。对于复杂一些的项目,生成的关系树是在太过复杂。你以为对象都是通过属性持有,实际上各类的闭包,函数指针等都会持有对象。
  2. pympler 感觉和objgraph差不多,不支持生成图像。
  3. gc.get_referents()/gc.get_referents()/gc.* 获取对象的引用计数及指向该对象的对象,以及其它分析函数。
    • 其它内存分析的库应当都是基于Python的gc模块实现。
  4. print('PromptSession count: ', len([o for o in obj if isinstance(o, PromptSession)])) 打印对象数量,确认是否被释放。

解决内存泄漏问题

要解决内存问题,关键还是找到存在内存泄漏的问题被谁给持有里,然后在需要销毁对象时释放该持有。如果想该对象持有不影响对象的生命周期(比如缓存),可以使用 weakref 库来创建弱引用。

Python Prompt Toolkit 的内存问题

出于性能等考虑 Python Prompt Toolkit 添加来大量的缓存。其中一些看似简单的缓存对象持有了其它对象的函数(函数指针),从而间接持有了其它对象,最终导致大量的对象未被释放。一般情况下一个程序只有一个 PromptSession 对象,该对象贯穿程序的整个生命周期,因此问题不容易察觉。但我的应用时一个服务端程序,需要反复创建和销毁 PromptSession 对象,问题将出现了。

我尝试用 weakref.WeakValueDictionary 改写它的缓存实现,实际过程中发现key和value都会持有对象。

目前的做法是用户断开服务器连接时进行一次缓存的清理。

在线使用的图片风格迁移工具

近期研究 ONNX Runtime Web 做的一个小东西。很多代码都“借鉴”于其他开源项目,解决了图片变形等问题。

使用深度学习模型做的图片风格迁移。使用 React 和 ONNX Runtime Web 开发,推理后端用的 WebAssembly ( CPU )。根据我的测试,用 WebGL 要慢不少,而且内存占用有些夸张。

在线访问: https://vicalloy.github.io/image-transformer/

项目地址: https://github.com/vicalloy/image-transformer

Note:

  1. 所有推理工作在浏览器完成,不需要消耗服务器资源。
  2. 推理的时候需要选择图片的输出大小。不同大小的输出用的模型不同。
    • 越大的输出尺寸需要耗费的算力越多(时间越长),测试的时候可以先用小尺寸看效果。
    • fast neural style这个模型也可以支持输出任意图片大小,不过动态参数模型太大,复杂度也高,不适合 Web 使用。

2021年Python工具链

1. Python虚拟环境:Poetry

一个类似Pipenv的Python虚拟环境和依赖管理的工具,据称改善了一些Pipenv的问题。对我而言,主要看重了Poetry可以对Python库打包的功能。毕竟对我而言书写 setup.py 并不是一件很让人愉快的事情。

2. 代码静态扫描:Flake8

Flake8使用起来非常简单,不用这么配置就可以直接使用,之后检查过程中遇到自己不需要的规则,加个例外就好。

Flake8支持插件,通过添加插件还可以让Flake8功能变的更为强大。

[2023-02 update]: 我的代码检查工具切换成了 ruff 。速度要快很多,且同样易用。

3. 代码自动格式化:Black

写代码时,我个人会尽量遵守 PEP8 ,但难保团队中有些人代码写的有些随意。为保证编码风格的统一,在代码提交前统一由Black对代码镜像格式化。自动格式化之后的代码可能会少了那么一点个性,但为了统一还是值得的。

4. Import规则检查&格式化工具:isort

Black不会对Python 的 import 语句进行排序和分段,这个工作就交给isort来做了。

5. 类型检查:Mypy

长久以来Python作为脚本语言,程序里没有类型信息,很多本可在编译阶段发现的问题被保留到运行时。Python在3.5之后开始支持 Type Hint 了。利用Mypy可以利用这些类型信息对程序进行校验。

6. 单元测试:pytest

相比 unittest ,pytest使用上更为方便。更为重要的是pytest兼容 unittest,似乎没有什么理由来拒绝pytest。 

7. 测试覆盖率:Coverage.py

代码覆盖率测试工具好像也没有第二个选择。

8. pre-commit

git commit 时调用flake8进行代码检查,调用black对代码进行格式化等操作。利用pre-commit从源头上杜绝有人把不合格的代码提交到代码库。

9. Docker、Gitlab-CI、GitHub Action、Travis CI

CI服务可根据自己的实际情况进行选择

将OpenVINO预训练模型转为为ONNX,并使用TVM进行优化

OpenVINO是Intel推出的一款深度学习工具套件。OpenVINO带来大量的预训练模型,使用这些预训练模型可以快速的开发出自己的AI应用。

不过既然是Intel出的东西,自然少不了和Intel平台深度绑定。OpenVINO主要针对Intel的CPU进行优化。虽然也可以支持GPU,但支持的是Intel家的GPU。Intel家的GPU,应当不用报太多期待了。

为了支持更丰富的硬件类型,可以将OpenVINO自带的预训练模型 转为ONNX格式,然后在做其他处理。

OpenVINO模型导出为ONNX

OpenVINO优化后的预训练模型无法直接转换为ONNX。不过好在Intel有提供模型的训练和导出工具,利用OpenVINO的训练工具导出ONNX

OpenVINO用于训练和导出的库为: https://github.com/openvinotoolkit/training_extensions

具体的操作方式参见项目的具体说明文档。

对照人脸检测的文档,导出人脸检测对应ONNX模型: https://github.com/openvinotoolkit/training_extensions/tree/develop/models/object_detection/model_templates/face-detection

注:导出目录里有 export/export/alt_ssd_export/ 两种模型。其中 export/alt_ssd_export/ 包含了OpenVINO特有的实现,在转换为其他推理引擎模型时会失败,因此后续工作使用 export/ 中的模型。

使用TVM对ONNX模型进行优化

针对TVM的VM进行优化

对于存在动态shape的模型,TVM无法进行编译。很不幸的是OpenVINO中物体检测相关的模型都存在动态shape。在TVM无法编译的情况下,可使用TVM的VM进行执行。

  • 注:
    • 关于VM的相关内容请阅读: https://tvm.apache.org/docs/dev/virtual_machine.html
    • TVM的文档比较欠缺(特别是VM相关的内容)。不过好在项目还在快速迭代过程中,提交的issue很快就可以得到回复。
    • 根据测试,使用VM模式,在CPU上TVM的速度甚至比用 ONNXRuntime 还要慢不少。不知道是否是跑在虚拟机上的关系。
import onnx
import time
import tvm
import numpy as np
import tvm.relay as relay
target = 'llvm -mcpu=skylake'
model_path = 'face-detection-0200.onnx'
onnx_model = onnx.load(model_path)
shape = [1,3,256,256]
input_name = "image"
shape_dict = {
        input_name: shape,
        }
mod, params = relay.frontend.from_onnx(onnx_model, shape_dict)
print(relay.transform.DynamicToStatic()(mod))
with tvm.transform.PassContext(opt_level=3):
    executable = relay.vm.compile(mod, target="llvm", target_host=None, params=params)
code, lib = executable.save()
with open("code.ro", "wb") as fo:
    fo.write(code)
lib.export_library("lib.so")

针对TVM进行编译和优化

如果你的模型可以正常编译,那就没必要采用VM模式了。直接编译理论上优化效果要好很多。这里采用的是TVM范例中给出的图片分类模型。

一个完整的模型优化和执行可以参考官方文档:Compiling and Optimizing a Model with the Python AutoScheduler

import onnx
import time
import tvm
import numpy as np
import tvm.relay as relay
target = 'llvm'
model_name = 'mobilenetv2'
model_path = f'{model_name}.onnx'
onnx_model = onnx.load(model_path)
mod, params = relay.frontend.from_onnx(onnx_model)
with relay.build_config(opt_level=3):
    graph, lib, params = relay.build(mod, target, params=params)
path_lib = f"./{model_name}.so"
lib.export_library(path_lib)
fo=open(f"./{model_name}.json","w")
fo.write(graph)
fo.close()
fo=open("./{model_name}.params","wb")
fo.write(relay.save_param_dict(params))
fo.close()

VM模式下加载和运行优化好的模型

加载前面导出的模型,并执行。


import onnx
import time
import tvm
import numpy as np
import tvm.relay as relay
def vmobj_to_array(o, dtype=np.float32):
    if isinstance(o, tvm.nd.NDArray):
        return [o.asnumpy()]
    elif isinstance(o, tvm.runtime.container.ADT):
        result = []
        for f in o:
            result.extend(vmobj_to_array(f, dtype))
        return result
    else:
        raise RuntimeError("Unknown object type: %s" % type(o))
shape = [1, 3, 224, 224]
model_path = 'face-detection-0200'
loaded_lib = tvm.runtime.load_module(f"{model_path}.tvm.so")
loaded_code = bytearray(open(f"{model_path}.tvm.code", "rb").read())
exe = tvm.runtime.vm.Executable.load_exec(loaded_code, loaded_lib)
ctx = tvm.cpu()
vm = tvm.runtime.vm.VirtualMachine(exe, ctx)
data = np.random.uniform(size=shape).astype("float32")
out = vm.run(data)
out = vmobj_to_array(out)
print(out)

Python多进程环境下日志模块导致死锁

近期公司的一个Python程序在启动新进程的时候总是会失败。在进程里可以看到对应的进程已经创建成功,但对应代码并未执行,且没有输入任何日志。

通过定位,发现问题源自Python的logging模块,在写文件模式下,logging模块是不支持多进程的。

问题产生原因

Python默认采用Fork方式创建新进程,在Fork新进程的时候会连同 也一同复制到新进程。

  1. 当主进程里有两个线程T1/T2,以及一个锁Lock1。
  2. 线程T2获取了锁Lock1,此时线程T1创建了一个新进程P2,此时Lock1被一同frok给了P2。
  3. P2执行时尝试等待Lock1解锁。由于线程T2不会被复制到P2,没有人给P2线程的Lock1解锁,导致P2死锁。

Python的logging模块在写文件时会加锁,由于锁被复制导致进程死锁。

注:由于创建新进程时锁会被复制,混用多进程和多线程时的加锁操作应当格外小心。

解决方案

根据Python的官方文档,logging模块不支持多进程模式下将日志保存到单一日志文件。多进程模式下日志保存方案,建议参考Python官方文档 Logging to a single file from multiple processes

参考资料: