TorchDynamo初探②:Torch.FX调研和实践

news2024/11/25 4:32:47

fbe7daf829237d58038f9b9f6a2864a4.jpeg

作者|strint

1

概要

torch.fx 是 PyTorch 官方发布的 Python 到 Python 的代码变换工具。如果你想做 Torch 代码变换,torch.fx 是首选工具。

torch.fx 会将 Torch 代码 trace 成 6 种基础的 node 组成的 graph,基于这个 graph 可以方便的做各种变换,变换后的 graph 可以再生成 torch 代码(一个 nn.Module),然后像普通的 nn.Module 一样去执行。

torch 2.0 新发布的 torch.compile(也即 TorchDynamo) 默认将代码转换成了 torch.fx 的 GraphModule,进一步加强了 torch.fx 的重要性。(相关文章:TorchDynamo初探①:Python ByteCode的动态修改

关键词:PyTorch,图变换,编译

2

最小用例

torch.fx 有三块基础功能。基础功能一是将 torch nn.Module 转换成 fx.GraphModule,该转换被称为 symbolic trace;基础功能二是中间表达和图改写;基础功能三是 Python 代码生成。

首先定义一个有代表性的 nn.Module,包括了 fx 要处理的6种基础操作:

import torch
# Simple module for demonstration
class MyModule(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.param = torch.nn.Parameter(torch.rand(3, 4))
        self.linear = torch.nn.Linear(4, 5)


    def forward(self, x):
        return self.linear(x + self.param).clamp(min=0.0, max=1.0)


module = MyModule()

然后使用 fx 的第一个基础功能 symbolic trace,它可以把 torch python 代码转换成符号化的(symbolic)表达,该表达的类型是 fx.GraphModule:

from torch.fx import symbolic_trace
# Symbolic tracing frontend - captures the semantics of the module
symbolic_traced : torch.fx.GraphModule = symbolic_trace(module)

fx.GraphModule 的特点是它执行计算时的行为和 nn.Module 相同,但是同时又具备一个内含的计算图,而该计算图是可以用图遍历的方式去操作的,中间表达和图改写都是基于该计算图去做的。打印 fx.GraphModule 可以看到上面 module 的图 IR 表达:

# High-level intermediate representation (IR) - Graph representation
print(symbolic_traced.graph)
"""
graph():
    %x : [#users=1] = placeholder[target=x]
    %param : [#users=1] = get_attr[target=param]
    %add : [#users=1] = call_function[target=operator.add](args = (%x, %param), kwargs = {})
    %linear : [#users=1] = call_module[target=linear](args = (%add,), kwargs = {})
    %clamp : [#users=1] = call_method[target=clamp](args = (%linear,), kwargs = {min: 0.0, max: 1.0})
    return clamp
"""

fx.GraphModule 内含的计算图可以再被转成 torch python 代码(也可以把计算图转成自定义的代码),即代码生成功能,比如下面就是上面 module 对应的 python 代码:

# Code generation - valid Python code
print(symbolic_traced.code)
"""
def forward(self, x):
    param = self.param
    add = x + param;  x = param = None
    linear = self.linear(add);  add = None
    clamp = linear.clamp(min = 0.0, max = 1.0);  linear = None
    return clamp
"""

后文再分别讨论这三块主要功能。

3

图生成(Symbolic Trace)

fx 构图的方法是 symbolic trace. 可以理解为把假的输入传入 nn.Module 或者函数,执行假的输入时,不是真的执行,而是记录执行的操作路径(Symbolic Trace),最后形成个完整执行记录就是一个图。

symbolic_trace函数的输入是 root 和 concrete_args. root 是要 trace 的代码。concrete_args 是可选地,可以传入一些假的输入以特化 trace。

trace 功能默认是用 Tracer 的 trace 方法实现的。它实现了 trace 功能,返回一个 fx.Graph,然后用 fx.Graph 和原 root 构造一个 fx.GraphModule 并返回。

def symbolic_trace(
    root: Union[torch.nn.Module, Callable[..., Any]],
    concrete_args: Optional[Dict[str, Any]] = None,
) -> GraphModule:
    tracer = Tracer()
    graph = tracer.trace(root, concrete_args)
    name = (
        root.__class__.__name__ if isinstance(root, torch.nn.Module) else root.__name__
    )
    return GraphModule(tracer.root, graph, name)

所以其实可以不用 symbolic_trace ,而是自己直接去调用 Tracer。如果需要自定义 trace 逻辑,其实可以用继承和改写 Tracer 的方式来改写 Tracer 的行为。

Tracer 的功能

Tracer 的主要方法是 trace,用来把输入的 nn.Module 或者函数转换成符号化的计算图(IR),trace 的实质就是随着值的传递把对应的操作记录下来。

trace 的机制依赖于把输入转换成抽象的值 Proxy,Proxy 起到代理 tensor 执行的作用。trace 的过程,即把 tensor 都转成 Proxy 在代码中传递,且 Proxy 可以输入常规的 torch 操作。

Proxy 输入常规的 torch 操作之所以可以工作,是依赖了 torch 下发操作的__torch_function__协议](https://github.com/pytorch/rfcs/blob/master/RFC-0001-torch-function-for-methods.md)。可以认为一个类型支持了 __torch_function__,就可以传给 torch 的常规函数去执行,而执行时调用的逻辑就在__torch_function__中定义。如此给 Proxy 的 __torch_function__ 定义好记录操作到图的逻辑,就可以完成 trace 功能(https://github.com/pytorch/pytorch/blob/de586001269fa04fa76ccc64964f676a25e120b2/torch/fx/proxy.py#L449)。

可以利用这个机制,实现一个极简的跟踪和打印 torch 操作的 ProxyTensor。对于加法,会把加法符号化:

import torch


class ProxyTensor(torch.Tensor):
    @classmethod
    def __torch_function__(cls, func, types, args=None, kwargs=None):
        if func.__name__ == 'add':
            print("\n=> torch function call:")
            print(f"==> function name: {func.__name__}")
            print(f"==> function args: ({', '.join((str(type(arg)) for arg in args))})")
            # 自定义张量相加的行为
            result = args[0].symbolic() + " + " + args[1].symbolic()
            return result
        else:
            # 对于其他运算,使用默认行为
            return super().__torch_function__(func, types, args=args, kwargs=kwargs)


    def symbolic(self):
        return "tensor(" + str(self.shape) + ", " + str(self.dtype) +")"


# 创建自定义张量
x = ProxyTensor([4, 5, 6])
y = ProxyTensor([1, 2, 3])


result = x - y
print(f"minus result: {result}")


result = x + y
print(f"add result: {result}")

执行一个减法,是常规的 torch tensor 运算:

minus result: ProxyTensor([3., 3., 3.])

执行加法时,则会对操作、输入、输出做自定义操作,而非执行 tensor 运算:

=> torch function call:
==> function name: add
==> function args: (<class '__main__.ProxyTensor'>, <class '__main__.ProxyTensor'>)
add result: tensor(torch.Size([3]), torch.float32) + tensor(torch.Size([3]), torch.float32)

Tracer 的 trace 实际也是在做类似的事情。首先把 nn.Module 或者函数的输入转换成 graph 中的 Node,然后把 Node 包装到 Proxy 里面,作为新的输入。之后 torch 的操作执行 Proxy 时,就会触发自定义的__torch_function__ 函数。Proxy 的自定义行为是把执行的操作记录为图中的 Node,然后操作把 Node 包装为 Proxy 作为操作结果去继续传递。如此便构造出了计算图。

另外值得考虑的是非内置的 nn.Module 和函数的嵌套调用,fx 中忽略了嵌套,所以 trace 到的都是 torch 内置的操作。如果你希望一个自定义操作为当做内置操作被 trace,可以使用 torch.fx.wrap 注册一下。

另外对于控制流、非 torch 内置操作,可以发现 trace 机制的局限性,他们会被 python 执行,但是 trace 不知道他们存在。所以 if 循环可能只记录了一个分支的执行,for 循环被展开,一个 python 的计算结果被当做常量传入torch内置操作。这是 trace 机制的局限性。

fx.GraphModule

trace 的返回结果是 fx.Graph,然后用其构造一个 fx.GraphModule 并返回。fx.GraphModule 继承自 nn.Module 所以其主要行为和 nn.Module 一致,特别的地方是它的 forward 是从 fx.Graph 生成的。另外它带有一个 graph 属性,用于获取其内部包含的计算图。还有个 code 属性,code 是 str 类型,是从 graph 生成的 python 文本代码,且 forward 方法是该文本代码经过编译得到的 。

symbolic_trace生成的 fx.GraphModule 通常当做普通的 nn.Module 使用即可,在使用时这么理解就够了。这个设计体现了 fx 良好的易用性。

自定义 Tracer

torch fx 还给 trace 的过程提供了自定义的空间,方法是继承和覆盖 Tracer。下面做下简介,通常使用是涉及不到的,所以可以忽略这部分。

有如下几种方法可以自定义:

  • create_node:Tracer 往 graph 中插入一个节点时都会调用它,它会返回一个 node,有如下 6 种类型的 node,这也是 trace 过程记录的基本单位;

    • placeholder,一般是整个被 trace 的 Module 或者函数的输入;

    • call_function,函数调用;

    • call_method,对象上的方法调用;

    • call_module,nn.Module 的调用;

    • get_attr,nn.Module 上的属性的获取;

    • output,一般是整个被 trace 的 Module 或者函数的输出;

  • create_proxy:如前文所示,所有操作( Operation)调用的输入、输出都是 Proxy,所以输入输出都会被转为 Proxy,转 Proxy 的过程都会调用 create_proxy 来实现。Proxy 对应上面 Node 多对应操作的抽象的返回结果,所以 Proxy 构造时会输入对应的 Node。

  • create_args_for_root:创建被 trace 的 Module 或者函数的输入;

  • create_arg:创建内部的函数的输入;

  • call_module:遇到一个 nn.Module 时调用 call_module 来触发对应的 node 创建等行为;

  • getattr:当从 nn.Module 上获取属性是会调用 getattr 来触发对应的 node 创建等行为;

以上方法的自定义和 Tracer 的行为耦合比较紧密,所以需要小心处理,结合 Tracer 的代码实现来做自定义。

4

图中间表达和图改写

上文中 trace torch 代码执行的过程,按 python 执行序去记录了操作序列,对于每个操作会生成一个 node,node 的类型是 fx.Node. 这些 node 总体形成了一个图,图的类型是 fx.Graph。

fx.Node 和 fx.Graph

fx.Node 和 fx.Graph 是 fx 中间表达的核心数据结构。上文的的例子中打印了一下 graph,可以看到一个完整的 graph 的文本表达,即中间表达。每一行对应一个 node(return 对应 output 类型的 node):

```python
# High-level intermediate representation (IR) - Graph representation
print(symbolic_traced.graph)
"""
graph():
    %x : [#users=1] = placeholder[target=x]
    %param : [#users=1] = get_attr[target=param]
    %add : [#users=1] = call_function[target=operator.add](args = (%x, %param), kwargs = {})
    %linear : [#users=1] = call_module[target=linear](args = (%add,), kwargs = {})
    %clamp : [#users=1] = call_method[target=clamp](args = (%linear,), kwargs = {min: 0.0, max: 1.0})
    return clamp
"""
```

fx.Graph 主要支持了一些图上的增删查改操作。支持用 nodes 属性获取图中所有 Node 的列表,支持用 create_node 添加一个新的 Node(也支持支持用 call_module、call_method 等语法糖直接添加特定类型的 Node),用 erase_node 删除一个 Node,用 inserting_after 或者 inserting_before 设置新 Node 的插入点,用 eliminate_dead_code 删除没有被使用的 Node,用 lint 做图结构的检查,用 on_generate_code 在代码生成时插入一些自定义操作。

fx.Node 代表图中的节点。fx.Node 的 op 属性可以获取 Node 的类型,上文创建 node 的部分也提到,有如下 6 种类型的 Node:

  • placeholder,一般是整个被 trace 的 Module 或者函数的输入;

  • call_function,函数调用;

  • call_method,对象上的方法调用;

  • call_module,nn.Module 的调用;

  • get_attr,nn.Module 上的属性的获取;

  • output,一般是整个被 trace 的 Module 或者函数的输出;

fx.Node 支持用 append 方法在该节点后面插入一个新 Node,支持用 prepend 在该节点前面插入一个新 Node,支持用 replace_all_uses_with 来把图中所有对本 Node 的依赖替换为一个新 Node,还支持一些其它的替换操作,支持用 format_node 来格式化打印一个 Node。

另外比较有价值的是 fx.Node target 属性记录了 node 对应的操作。对于 placeholder、output、call_method,target 是个普通的字符串名字;对于 call_function,target 是函数本身;而对于 call_module 和 get_attr,target 也是字符串,但是该字符串是查找对应 module 或者属性对象的 key,这里的设计不太好需要适应下,假设gm是 GraphModule 的实例,如下方法才能通过 key 找到 call_module 和 get_attr Node 对应的实例:

# node 为 call_module 时,其 Module 实例查找方法
modules = gm.named_modules()
module = modules[node.target]


# node 为 get_attr 时,其 attr 实例查找方法
getattr(gm, node.target)

fx.Node 的 meta 属性里面包含了 node 相关的对象信息、代码调用栈信息。对象信息可以帮助拿到对象实例的值而代码调用栈可以帮助确认当前 node 对应的代码位置。这两个信息对于 Debug 非常有帮助。

图遍历模式

图遍历模式是最典型的图改写模式。可以用 fx.Graph.nodes 来获取图中节点并加以改写。如下是一个把 add 操作替换成 bitwise_and 操作的图改写例子。

import torch
from torch.fx import symbolic_trace
import operator


# 定义一个普通的 module
class M(torch.nn.Module):
    def forward(self, x, y):
        return x + y, torch.add(x, y), x.add(y)


# trace 一下
traced = symbolic_trace(M())


# 要匹配的 target 列表
patterns = set([operator.add, torch.add, "add"])


# 遍历 fx.Graph 的 Node 列表并修改
for n in traced.graph.nodes:
    # 如果当前 Node 的 target 符合 add
    if any(n.target == pattern for pattern in patterns):
        # 在当前 Node 的后面插入 bitwise_and Node
        with traced.graph.inserting_after(n):
            new_node = traced.graph.call_function(torch.bitwise_and, n.args, n.kwargs)
            n.replace_all_uses_with(new_node)
        # 清理掉过时的 Node
        traced.graph.erase_node(n)
# 重新编译下 GraphMoudle
# 根据新的图做代码生成,这样就得到了新的 GraphModule 了
traced.recompile()

上面注释了一个典型的 graph 图遍历修改图的模式。更多例子可以参考这个链接。

另外如果通常需要做一些复杂输入的通用处理,这时 map_aggregate 函数提供了对参数的通用变换工具函数。对于一个由 node 组成的 tuple/list/dict 等类型的输入,你可以提供一个 node 处理函数 fn 给 map_aggregate,然后 map_aggregate 返回一个和原输入 tuple/list/dict 同结构的输入,这个新的输入中的每个 node 都是被 fn 变换过的。该功能类似 oneflow 中的 ArgsTree.

Interpreter 模式

Interpreter 模式提供了一种边执行边修改图的模式。其实质是我们可以遍历图中的节点,且同时挨个执行图中的节点。上文也提到了,可以通过 Node.target 属性获得节点的实例,比如获取 nn.Module,然后执行该实例即可。这里提供了一个通过执行 Node 来记录 Node 的实际输出 tensor 的 shape 和 dtype 的例子ShapeProp。可以看到其核心是遍历 Node 和 执行 Node:

for node in self.graph.nodes:
    if node.op == 'placeholder':
        result = next(args_iter)
    elif node.op == 'get_attr':
        result = fetch_attr(node.target)
    elif node.op == 'call_function':
        # load_arg 可以获取实际的 tensor,然后输入 target 做 operator 的执行
        result = node.target(*load_arg(node.args), **load_arg(node.kwargs))
    elif node.op == 'call_method':
        self_obj, *args = load_arg(node.args)
        kwargs = load_arg(node.kwargs)
        result = getattr(self_obj, node.target)(*args, **kwargs)
    elif node.op == 'call_module':
        result = self.modules[node.target](*load_arg(node.args), **load_arg(node.kwargs))
    
    if isinstance(result, torch.Tensor):
        # 记录执行结果的 shape 和 dtype
        node.shape = result.shape
        node.dtype = result.dtype

Interpreter 模式提供了一个语法糖fx.Interpreter,它实现了上面的图遍历过程,然后支持重载不同 Node 类型的行为,如此可以自定义执行一个 Node 的逻辑。

fx.Interpreter 接受输入一个 fx.GraphModuel,然后用 run 方法来执行 GraphModule:

def fn(x):
    return torch.sigmoid(x).neg()


gm = torch.fx.symbolic_trace(fn)
input = torch.randn(3, 4)


class MyInterpreter(fx.Interpreter):
    pass


result = MyInterpreter(gm).run(input)

run 实际在遍历图中的 Node,然后对该 Node 调用 run_node 方法,而 run_node 方法则调用了各种类型的 Node 的执行方法:

run()
    +-- run_node()
        +-- placeholder()
        +-- get_attr()
        +-- call_function()
        +-- call_method()
        +-- call_module()
        +-- output()

run_node 和各种类型的 node 的执行方法都可以重载。使用 Interpreter 来实现 ShapeProp,可以看出不用自己写图遍历了:

class ShapePropInterpreter(fx.Interpreter):
    def run_node(self, n : Node) -> Any:
        result = super().run_node(n)
        if isinstance(result, torch.Tensor):
            # 记录执行结果的 shape 和 dtype
            n.shape = result.shape
            n.dtype = result.dtype
        return result


result = ShapePropInterpreter(gm).run(input)

另外也可以用 Interpreter 实现图改写的效果,如下就是把原来的 sigmoid 改成了 neg 操作:

class NegSigmSwapInterpreter(fx.Interpreter):
    def call_function(self, target : Target,
                      args : Tuple, kwargs : Dict) -> Any:
        if target == torch.sigmoid:
            # 这里传入的参数是实际值
            return torch.neg(*args, **kwargs)
        return super().call_function(n)


# 执行 Interpreter
result = NegSigmSwapInterpreter(gm).run(input)

Interpreter 可以边执行,边操作图。但是它的缺点是可以修改图的实际执行,但是不能改图结构。

Transformer 模式

Interpreter 模式另外一个缺点是它是即时执行的,也没有改变图的结构。如果想修改图的结构,就可以使用 Transformer 模式。

fx.Transformer 继承自 Interpreter,所以支持的重载接口类似。不同的是,它实际在做符号化的执行,且再创建一个新的图。

传入一个原始的 GraphModule 到 Transformer,然后调用 transform 方法。会创建一个新的图出来了,然后按原来图的节点顺序执行,返回结果会用于在新的图中创建一个新节点。最后就得到了一个新的 GraphModule。

class NegSigmSwapXformer(fx.Transformer):
    def call_function(self, target : 'Target', args : Tuple[Argument, ...], kwargs : Dict[str, Any]) -> Any:
        if target == torch.sigmoid:
            # 这里传入的参数是 Proxy
            return torch.neg(*args, **kwargs)
        return super().call_function(n)


# 得到了一个 sigmoid 被替换为 neg 操作的 GraphModule
transformed : torch.nn.Module = NegSigmSwapXformer(gm).transform()

fx.Transformer 提供了一种便捷的 Node 到 Node 图改写方式。

5

Python 代码生成

Python 代码生成在 GraphModule 的 recompile 方法调用时触发。它是 fx 的内部行为,使用时通常不用关注,这里介绍下主要实现技巧。

python 代码生成在做的事情就是把 graph 转成 code:

# High-level intermediate representation (IR) - Graph representation
print(symbolic_traced.graph)
"""
graph():
    %x : [#users=1] = placeholder[target=x]
    %param : [#users=1] = get_attr[target=param]
    %add : [#users=1] = call_function[target=operator.add](args = (%x, %param), kwargs = {})
    %linear : [#users=1] = call_module[target=linear](args = (%add,), kwargs = {})
    %clamp : [#users=1] = call_method[target=clamp](args = (%linear,), kwargs = {min: 0.0, max: 1.0})
    return clamp
"""
print(symbolic_traced.code)
"""
def forward(self, x):
    param = self.param
    add = x + param;  x = param = None
    linear = self.linear(add);  add = None
    clamp = linear.clamp(min = 0.0, max = 1.0);  linear = None
    return clamp
"""

这转换的过程是一对一的,一个 Node 会被转换成对应的 Python 代码。其核心函数是 fx.Graph 中的 emit_node 函数,以 call_method Node 为例:

elif node.op == 'call_method':
    assert isinstance(node.target, str)
    body.append(
        f'{repr(node)}{maybe_type_annotation} = {_format_target(repr(node.args[0]), node.target)}'
        f'({_format_args(node.args[1:], node.kwargs)})')

上面的方法根据 node 中信息,在 body 中添加了 python 文本代码,把 Node 信息:

%clamp : [#users=1] = call_method[target=clamp](args = (%linear,), kwargs = {min: 0.0, max: 1.0})

转换成了 Python 代码:

clamp = linear.clamp(min = 0.0, max = 1.0);  linear = None

这部分 emit_node 逻辑是在 fx.Graph 的 python_code 函数中调用的,python_code 函数返回一个 python_code 对象,包含了 python 源代码和全局对象数据。然后执行如下操作,就把图生成的代码赋值给了 GraphModule。

# 生成 python 代码对象,graph 对应一个 fx.Graph
python_code = graph.python_code(root_module='self')
# python 代码文本
code = python_code.src
# python 代码全局对象
globals = python_code.globals
# 使用 python 字节码编译器编译和加载 python 代码
exec(compile(code, key, 'exec'), globals)
# 从中获取编译好的总函数 forward
forward_fn = globals_copy['forward']

最后用 forward_fn 替换 GraphModule 的 forward 的方法,就得到了一个和 graph 中执行逻辑相同的 GraphModule 了。

6

torch.fx 和 torch.compile

在 torch 2.0 的 torch.compile (TorchDynamo) 功能下,一个函数或者 nn.Module输入 torch.compile 编译时,可以自定义一个编译器后端。

如下的custom_backend即自定义的编译逻辑。torch.compile 会把对应的 torch 代码 trace 成 fx.GraphModule 对象,然后传入 custom_backend 函数,这样你就可以根据 fx.GraphModule 自定义编译逻辑,生成一个自定义的函数,返回给 torch.compile。下面例子中的 opt_model 第一次执行时,会触发custom_backend 执行,获取一个自定义的函数(经过编译优化的函数)并缓存下来,后面执行时,就可以直接使用编译优化的函数做执行,达到优化执行的效果。

from typing import List
def custom_backend(gm: torch.fx.GraphModule, example_inputs: List[torch.Tensor]):
    print("custom backend called with FX graph:")
    print(gm.graph)
    return gm.forward


opt_model = torch.compile(init_model(), backend=custom_backend)

7

总结

torch.fx 是 PyTorch 官方发布的 Python 到 Python 的代码变换工具。它提供了 trace 代码生成图、改写图、再生成新的 Python 代码的工具。灵活性和易用性都很高。本文介绍了其核心功能和一些实践技巧。

OneFlow 利用 torch.fx 和 torch.compile 做 Torch 代码到 OneFlow 代码的转换工作,以更简单的编译和加速 Torch 代码。

参考

[1]. torch fx 官方文档. https://pytorch.org/docs/stable/fx.html

[2]. torch.fx: Practical Program Capture and Transformation for Deep Learning in Python. https://arxiv.org/pdf/2112.08429.pdf

[3]. torch fx 应用于将 torch 转成 oneflow. https://github.com/Oneflow-Inc/diffusers/pull/237

[4]. 适配PyTorch FX,OneFlow让量化感知训练更简单

其他人都在看

试用OneFlow: github.com/Oneflow-Inc/oneflow/

51bc4e7350d46876738707d38e94f263.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/993692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

01 PHP基础知识讲解

一 php基础知识 PHP文件的默认拓展名是“php”。 PHP文件中包含HTML标记、PHP标记、PHP代码以及空格和注释。 PHP标记&#xff1a;开始标记<?php 结束标记 ?> 中间内容是PHP代码。 PHP代码&#xff1a;学习第一个指令 echo 功能是用于输出字符串 。 语句结束符&a…

从零开始-与大语言模型对话学技术-gradio篇(4)

前言 本文介绍「星火杯」认知大模型场景创新赛中的落选项目- AI命理分析系统&#xff0c;属于个人娱乐练手。总结提炼了往期文章精华并发掘出新的知识。 包括本地部署版本和Web在线版本&#xff0c;两种打包方式基于 半自动化使用.bat手动打包迁移python项目 如何把 Gradio …

Minio集群搭建

一、官方文件 1、minio官网 https://min.io/ 2、中文文档 http://docs.minio.org.cn/docs/ 3、集群原理 二、集群部署 1、在每台服务器上创建minio目录 mkdir -p /app/minio/{run,data1,data2} && mkdir -p /etc/minio2、下载或者上传下载好的minio二进制文件 https…

net/http库中request.RemoteAddr的值不确定性-【Golang踩坑笔记】

环境信息&#xff1a; Go 1.20Windows 11 x64 代码示例 // 这里的r是框架传入的request&#xff0c;其中封装了net/http下的request.go中的Request fmt.Println("r.RemoteAddr:", r.RemoteAddr) // 本地执行时,该值可能是[::1]:port也可能是127.0.0.1:port 当在…

安装GPU驱动,CUDA Toolkit和配置与CUDA对应的Pytorch

如果有帮助,记得回来点个赞 目录 1.安装指定GPU驱动如果安装的GPU CUDA Version和CUDA Toolkit版本已经冲突怎么办? 2.安装指定版本的CUDA Toolkit如果我安装了CUDA Toolkit之后nvcc -V仍然显示旧的CUDA Toolkit版本怎么办? 3.安装与CUDA对应的Pytorch 1.安装指定GPU驱动 &…

C++之string

目录 1、了解string 2、string相关函数 3、相关函数的使用 ①构造函数 ②赋值 ③>>&#xff0c;<< ④operator[] ⑤size ⑥iterator(迭代器) ⑦push_back ⑧append ⑨ ⑩capacity ⑪reserve ⑫resize ⑬insert ⑭erase ⑮c_str ⑯find ⑰substr…

简化转换器:使用您理解的单词进行最先进的 NLP — 第 1 部分 — 输入

一、说明 变形金刚是一种深度学习架构&#xff0c;为人工智能的发展做出了杰出贡献。这是人工智能和整个技术领域的一个重要阶段&#xff0c;但也有点复杂。截至今天&#xff0c;变形金刚上有很多很好的资源&#xff0c;那么为什么要再制作一个呢&#xff1f;两个原因&#xff…

代码随想录第43天|416. 分割等和子集,1049. 最后一块石头的重量 II, ​494.目标和,​ 474.一和零(一窍不通)

416. 分割等和子集 思路 本题是01背包的应用题 背包的体积为sum / 2背包要放入的商品&#xff08;集合里的元素&#xff09;重量为 元素的数值&#xff0c;价值也为元素的数值背包如果正好装满&#xff0c;说明找到了总和为 sum / 2 的子集。背包中每一个元素是不可重复放入…

(其他) 剑指 Offer 67. 把字符串转换成整数 ——【Leetcode每日一题】

❓ 剑指 Offer 67. 把字符串转换成整数 难度&#xff1a;中等 写一个函数 StrToInt&#xff0c;实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。 首先&#xff0c;该函数会根据需要丢弃无用的开头空格字符&#xff0c;直到寻找到第一个非空格的字符为…

C++学习笔记(堆栈、指针、命名空间、编译步骤)

C 1、堆和栈2、指针2.1、指针的本质2.2、指针的意义2.3、清空指针2.4、C类中的this 3、malloc and new4、命名空间4.1、创建命名空间4.2、使用命名空间 5、编译程序的四个步骤5.1、预处理5.2、编译5.3、汇编5.4、链接 1、堆和栈 堆&#xff08;heap&#xff09;和栈&#xff0…

领域驱动设计:DDD与微服务的关系

文章目录 基础概念软件架构模式的演进微服务设计和拆分的困境为什么 DDD 适合微服务&#xff1f;DDD 与微服务的关系 基础概念 DDD 虽然历史很久了&#xff0c;但它与微服务和中台设计的结合&#xff0c;却是一片很新的领域。早在 2003 年就诞生的 DDD&#xff0c;怎么来指导“…

基于spring boot+ vue开发的位置数据展现和分析平台源码 UWB源码

spring boot vue位置数据展现和分析平台源码 UWB室内外高精度定位系统源码 智慧工厂是现代工厂信息化发展的新阶段&#xff0c;基于UWB定位技术&#xff0c;融合位置物联网、GIS可视化等技术&#xff0c;实现对人员、物资精确管理。在重点区域设置电子围栏&#xff0c;无权限…

【Mysql】数据库第一讲(服务器数据库的安装和基础操作介绍)

数据库基础 &#x1f361;1.CentOs服务器数据库的安装&#x1f367;2.基础使用&#x1f368; 2.1 服务器&#xff0c;数据库&#xff0c;表关系&#x1f366;2.2使用案例&#xff1a; &#x1f967;3.数据库分类&#x1f9c1;4.存储引擎&#x1f370;4.Mysql库的操作&#x1f3…

YOLO目标检测——密集人群人头数据集+已标注yolo格式标签下载分享

实际项目应用&#xff1a;城市安防、交通管理、社会研究、商业应用、等多个领域数据集说明&#xff1a;YOLO密集人群人头目标检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富&#xff0c;图片格式为jpg&#xff0c;共4300张图片。标注说明&#xff1a…

【web开发】6、Django(1)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、Django是什么&#xff1f;二、使用步骤1.安装Django2.创建项目3.创建app4.快速上手 数据库操作1.安装第三方模块2.自己创建数据库3.DJango链接数据库4.DJango操…

ubuntu下查看文件(夹)大小的命令

记录下自己常用的查看文件夹和文件大小的命令。 1、查看当前所处的文件夹的整个大小&#xff08;只看当前文件夹大小&#xff09; du -sh2、查看当前文件夹中各个文件的大小 用-l或者-lh都行&#xff0c;加个h会增加可读性&#xff0c;如果想看具体某个文件&#xff0c;在后…

wires hark抓包内容解析

1.Frame 22001&#xff1a;这是该数据包的序号&#xff0c;表示wires hark已经捕获并显示了22001个数据包&#xff1b; 2.225 bytes on wire (1800 bits)&#xff1a;该数据的原始大小、以字节和比特显示&#xff1b; 3.225 bytes captured (1800 bits)&#xff1a;wires har…

元宇宙Web3.0科普---MoneyKing链游平台新格局介绍

众所周知&#xff0c;当前元宇宙“行业化”概念爆发后&#xff0c;当前社会甚至全世界再次步入一个全新的世界格局分水岭&#xff1b;随之而来的&#xff0c;包括了元宇宙具像化落地的Web3.0概念。 如果有人不懂什么是元宇宙&#xff0c;不懂什么是Web3.0&#xff0c;小编用最…

多线程与高并发——并发编程(5)

文章目录 五、线程池1 什么是线程池2 JDK自带的构建线程池的方式2.1 FixedThreadPool2.2 SingleThreadExecutor2.3 CachedThreaPool2.4 ScheduleThreadPool2.5 WorkStealingPool3 ThreadPoolExecutor应用&源码剖析3.1 为什么要自定义线程池3.2 ThreadPoolExecutor应用3.3 T…

数据透视表如何让多个行标签并列显示?

数据透视表如何让多个行标签并列显示&#xff1f; “数据透视表工具 - 报表布局" 这里有三种格式&#xff1a;&#xff08;1&#xff09;以压缩形式显示&#xff1b;&#xff08;2&#xff09;以大纲&#xff1b;&#xff08;3&#xff09;以表格形式。 选择“以表格显…