首页 > 其他分享 >Uniswap V3:流动性提供

Uniswap V3:流动性提供

时间:2025-01-21 17:02:08浏览次数:3  
标签:流动性 tickUpper liquidityDelta V3 params Uniswap tick 我们 tickLower

作者:WongSSH

引言

本系列文章将带领读者从零实现Uniswap V3核心功能,深入解析其设计与实现。 主要参考了 Constructor | Uniswap V3 Core Contract Explained 系列教程、 Uniswap V3 Development Book 和 Paco 博客中的相关内容。所有示例代码可在 clamm 代码库中找到,以便实践和探索。

流动性提供

mint 函数用于向流动性池内增加流动性。在本节中,我们将介绍 mint 函数的构成及相关数学计算与代码。另外,为了简化文章内容,我们并不会在本文内涉及手续费和预言机逻辑,同时我们也去掉了 mint 函数内的回调函数。所以,我们可以得到以下 mint 函数的定义:

function mint(
    address recipient,
    int24 tickLower,
    int24 tickUpper,
    uint128 amount
) external lock returns (uint256 amount0, uint256 amount1) {}

mint 函数定义内包含一个 lock 修饰器,该修饰器是为了避免重入的,其使用了 slot0 内的unlocked 状态变量:

modifier lock() {
    require(slot0.unlocked, 'LOK');
    slot0.unlocked = false;
    _;
    slot0.unlocked = true;
}

接下来,我们所编写的代码都位于 mint 函数内部:

require(amount > 0);
(, int256 amount0Int, int256 amount1Int) = _modifyPosition(
    ModifyPositionParams({
        owner: recipient,
        tickLower: tickLower,
        tickUpper: tickUpper,
        liquidityDelta: int256(uint256(amount)).toInt128()
    })
);

amount0 = uint256(amount0Int);
amount1 = uint256(amount1Int);

if (amount0 > 0) {
    IERC20(token0).transferFrom(msg.sender, address(this), amount0);
}
if (amount1 > 0) {
    IERC20(token1).transferFrom(msg.sender, address(this), amount1);
}

此处的 _modifyPosition 函数内部实际上计算了铸造 amount 数量的 Uniswap V3 LP 所需要的 token0 和 token1 的数量。由于我们没有使用 uniswap v3 的回调方案,所以此处直接使用了 transferFrom 将固定数量的代币转移给池子。上述代码对于某些代币而言存在漏洞,建议用户不要在生产环境内使用。

在 Uniswap V3 的官方实现内,这部分使用了回调函数处理,但在本文中,我们并不会实现回调函数的相关逻辑。因为回调函数与 Uniswap v3 的外围合约存在一些依赖关系。

我们首先实现较为简单的 toInt128() 函数,该函数用于将 uint128类型转化为 int128类型。我们首先创建 clamm/src/libraries/SafeCast.sol文件,并在该文件内输入以下内容:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;

library SafeCast {
    function toInt128(int256 y) internal pure returns (int128 z) {
        require((z = int128(y)) == y);
    }
}

上述代码通过判断转化后的 z 和 y 是否一致来实现安全的类型转化。然后,我们需要在 CLAMM.sol 内使用 import "./libraries/SafeCast.sol";语句导入该库,并且使用 using SafeCast for int256;语句将其使用在 int256类型上。

在 mint 函数内,目前唯一没有实现的就是 _modifyPosition 函数,这是一个相当复杂的函数。本节后续所有内容都将围绕该函数展开。我们先给出该函数的定义:

function _modifyPosition(ModifyPositionParams memory params) private returns (Position.Info storage position, int256 amount0, int256 amount1) {}

在此定义内,我们会发现 ModifyPositionParams结构体和 Position.Info  都没有此前进行过定义。所以第一步,我们先将这两个结构体进行定义。首先定义 ModifyPositionParams 结构体,该结构体用于传递用户流动性所在区间和流动性变化等参数。此处的 liquidityDelta是 int128 类型,是因为我们在提取流动性时也会使用此结构体,此时的 liquidityDelta就是负数,代表用户提取的流动性数量。

struct ModifyPositionParams {
    // the address that owns the position
    address owner;
    // the lower and upper tick of the position
    int24 tickLower;
    int24 tickUpper;
    // any change in liquidity
    int128 liquidityDelta;
}

接下来,我们定义 Position.Info结构体。创建 clamm/src/libraries/Position.sol文件并写入以下内容。注意,在本节中,我们并不会涉及手续费计算问题,所以实际上只会使用 Info中的 liquidity参数。此处,我们也编写了 get 方法,该方法用于在存储映射内检索存储的结构体。

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;

library Position {
    struct Info {
        // the amount of liquidity owned by this position
        uint128 liquidity;
        // fee growth per unit of liquidity as of the last update to liquidity or fees owed
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
        // the fees owed to the position owner in token0/token1
        uint128 tokensOwed0;
        uint128 tokensOwed1;
    }

    function get(mapping(bytes32 => Info) storage self, address owner, int24 tickLower, int24 tickUpper)
        internal
        view
        returns (Position.Info storage position)
    {
        position = self[keccak256(abi.encodePacked(owner, tickLower, tickUpper))];
    }
}

然后,可以在 clamm/src/CLAMM.sol 主合约使用 import  "./libraries/Position.sol";  导入上述库,并使用以下代码创建相关存储映射:

using Position for Position.Info;
mapping (bytes32 => Position.Info) public positions;
using Position formapping(bytes32 => Position.Info);

完成上述工作后,我们就可以编写 _modifyPosition 中的逻辑代码:

function _modifyPosition(ModifyPositionParams memory params)
    private
    returns (Position.Info storage position, int256 amount0, int256 amount1)
{
    checkTicks(params.tickLower, params.tickUpper);
    Slot0 memory _slot0 = slot0;

    position = _updatePosition(
        params.owner,
        params.tickLower,
        params.tickUpper,
        params.liquidityDelta,
        _slot0.tick
    );
}

上述代码中,我们暂时缺失了 amount0和 amount1的计算,我们会在后文进行补充。

此处的 checkTicks函数用于确定输入的参数是否正确,其具体实现如下:

function checkTicks(int24 tickLower, int24 tickUpper) private pure {
    require(tickLower < tickUpper, "TLU");
    require(tickLower >= TickMath.MIN_TICK, "TLM");
    require(tickUpper <= TickMath.MAX_TICK, "TUM");
}

之后,我们可以看到 _modifyPosition使用了 Slot0 memory _slot0 = slot0;将位于存储内的 slot0 缓存到内存内部。如果读者希望更加深入的了解该优化的原理,可以参考笔者之前编写的 Solidity Gas 优化清单及其原理:存储、内存与操作符一文 。

而 _updatePosition则较为复杂,我们将一步步构建,我们首先构造一个最简单的 _updatePosition函数,实现如下:

function _updatePosition(address owner, int24 tickLower, int24 tickUpper, int128 liquidityDelta, int24 tick)
    private
    returns (Position.Info storage position)
{
    position = positions.get(owner, tickLower, tickUpper);

    // TODO: Fee
    uint256 _feeGrowthGlobal0X128 = 0;
    uint256 _feeGrowthGlobal1X128 = 0;


    // TODO: Fee
    position.update(liquidityDelta, 0, 0);
}

此处,跳过了所有的手续费计算环节,我们会在未来介绍此部分。此处使用  position.update  函数, 因为目前我们跳过了所有的手续费计算逻辑, 所以 position.update的唯一功能就是更新 position 内的 liquidity字段。我们需要在 clamm/src/libraries/Position.sol内编写如下函数:

function update(
    Info storage self,
    int128 liquidityDelta,
    uint256 feeGrowthInside0X128,
    uint256 feeGrowthInside1X128
) internal {
    Info memory _self = self;

    uint128 liquidityNext;
    if (liquidityDelta == 0) {
        require(_self.liquidity > 0, "0 liquidity");
        liquidityNext = _self.liquidity;
    } else {
        liquidityNext = liquidityDelta < 0
            ? _self.liquidity - uint128(-liquidityDelta)
            : _self.liquidity + uint128(liquidityDelta);
    }

    if (liquidityDelta != 0) self.liquidity = liquidityNext;
    self.feeGrowthInside0LastX128 = feeGrowthInside0X128;
    self.feeGrowthInside1LastX128 = feeGrowthInside1X128;
}

上述函数中需要注意的是:

  1. 在 liquidityDelta == 0且 _self.liquidity == 0的情况下,不需要更新当前的 position的 liquidity字段

  2. liquidity 是一个 uint128类型,但 liquidityDelta是一个 int128 类型,我们需要手动处理当 liquidityDelta < 0 的情况

回到 _updatePosition函数,接下来我们要在该函数内部完成 tick部分的更新。 tick记录了流动性等信息,我们首先实现 tick 的结构体定义:

struct Info {
    uint128 liquidityGross;
    int128 liquidityNet;
    uint256 feeGrowthOutside0X128;
    uint256 feeGrowthOutside1X128;
    bool initialized;
}

其中 liquidityGross代表该 tick 内具有的流动性数量,该数值主要用于判断当前 tick是否还存在流动性。 initialized表示当前 tick 是被初始化。 feeGrowthOutside0X128 和 feeGrowthOutside1X128用于计算手续费,我们目前并不会编写此部分代码。

而 liquidityNet是一个重要的变量,该变量用于计算当前池子内活跃的流动性数量。liquidityNet的运作原理非常有趣,观察下图(该图来自 Uniswap v3 详解(一):设计原理

上图内显示了两段不同的区间流动性,其中  是自 a tick 开始到 c tick 结束的价格区间,而  是从 b tick 开始到 d tick 结束的价格区间。当用户添加  流动性时,我们会在流动性区间的下限 a tick 的的 liquidityNet字段内记录添加的流动性数量 500,而在区间上限记录添加的流动性的相反数。让我们设想当起流动性池内启用了 L 单位流动性,且当前的价格为 p ,当价格 p 从 a 点的左侧移动到 a 点的右侧时, 单位的流动性会被启用,此时池子内启用的流动性为 。当进一步向右移动超过 b 点时,池子内启用的流动性为 。当价格进一步向右移动超过过 c 点时, 段流动性完全退出,此时直接使用 c 点记录的的 liquidityNet = -500 就可以计算出当前池子内启用的流动性,数值为 。当价格进一步向右移动超过 d 点时, 也退出,最终流动性变为 。所以我们只需要记录修改价格区间的下限和上限就可以表示区间流动性。

接下来,我们把上述介绍的一些逻辑进行实现。我们先实现 tick 的更新函数,该函数定义如下:

function update(
    mapping(int24 => Tick.Info) storage self,
    int24 tick,
    int24 tickCurrent,
    int128 liquidityDelta,
    uint256 feeGrowthGlobal0X128,
    uint256 feeGrowthGlobal1X128,
    bool upper,
    uint128 maxLiquidity
) internal returns (bool flipped) {}

此处我们删除了 Uniswap V3 原版代码内的预言机部分参数。该函数的返回值 flipped代表更新后,tick 的状态是否有变化。当出现以下情况时,filpped为 true:

  1. update前,tick 的流动性,即 liquidityGross参数为零值,但 update后,liquidityGross不为零

  2. update前,tick 的流动性,即 liquidityGross参数为非零值,但   update后,liquidityGross为零

在有了上述函数定义后,我们开始编写函数的逻辑部分:

Tick.Info storage info = self[tick];

uint128 liuquidityGrossBefore = info.liquidityGross;
uint128 liquidityGrossAfter = liquidityDelta < 0
    ? liuquidityGrossBefore - uint128(-liquidityDelta)
    : liuquidityGrossBefore + uint128(liquidityDelta);

require(liquidityGrossAfter <= maxLiquidity, "liquidity > max");

// flipped = (liuquidityGrossBefore == 0 && liquidityGrossAfter != 0)
//     || (liuquidityGrossBefore != 0 && liquidityGrossAfter == 0);

flipped = (liquidityGrossAfter == 0) != (liuquidityGrossBefore == 0);

if (liuquidityGrossBefore == 0) {
    info.initialized = true;
}

info.liquidityGross = liquidityGrossAfter;

info.liquidityNet = upper 
 ? info.liquidityNet - liquidityDelta 
 : info.liquidityNet + liquidityDelta;

此处我们首先计算了 liquidityGrossAfter即更新后的 tick 对应的流动性数值,然后计算了 flipped变量。我们使用了异或运算简化了原有的 (liuquidityGrossBefore == 0 && liquidityGrossAfter != 0) || (liuquidityGrossBefore != 0 && liquidityGrossAfter == 0);的复杂逻辑计算。本质原因是因为以下布尔计算的成立:

Image

在上述代码的最后,我们计算了最重要的 liquidityNet 参数。在  update传参过程中,我们使用了 upper标识是否为流动性区间上限。根据上文的推导,当 tick 位于某一流动性区间上限时,我们需要减去 liquidityDelta,反之则加上 liquidityDelta 。上述代码的最后一段就完成了此任务。

在 Tick.sol内,我们还缺少一个移除 tick 的函数 clear,该函数相当简单:

function clear(mapping(int24 => Tick.Info) storage self, int24 tick) internal {
    delete self[tick];
}

当我们完成上述 Tick.sol内的代码后,我们首先在clamm.sol内导入上述定义和相关库函数:

using Tick formapping(int24 => Tick.Info);
mapping(int24 => Tick.Info) public ticks;

然后,我们就可以继续完成我们的 _updatePosition函数内部的 tick 更新逻辑:

function _updatePosition(address owner, int24 tickLower, int24 tickUpper, int128 liquidityDelta, int24 tick)
    private
    returns (Position.Info storage position)
{
    position = positions.get(owner, tickLower, tickUpper);

    // TODO: Fee
    uint256 _feeGrowthGlobal0X128 = 0;
    uint256 _feeGrowthGlobal1X128 = 0;

    bool flippedLower;
    bool flippedUpper;

    if (liquidityDelta != 0) {
        flippedLower = ticks.update(
            tickLower,
            tick,
            liquidityDelta,
            _feeGrowthGlobal0X128,
            _feeGrowthGlobal1X128,
            false,
            maxLiquidityPerTick
        );

        flippedUpper = ticks.update(
            tickUpper, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, true, maxLiquidityPerTick
        );
    }

    // TODO: Fee
    position.update(liquidityDelta, 0, 0);

    if (liquidityDelta < 0) {
        if (flippedLower) {
            ticks.clear(tickLower);
        }
        if (flippedUpper) {
            ticks.clear(tickUpper);
        }
    }
}

此处,使用传入的 tickLower 和 tickUpper来更新 ticks。并在最后使用 flippedLower和 flippedUpper 的情况清空了不包含流动性的 tick。

至此,我们完成了 position和 tick的更新。position用来表示用户添加的流动性区间的相关数据,而tick则主要用于存储流动性和手续费相关数据。

接下来,我们需要计算指定流动性所需要的代币数量。我们首先推导一下 Uniswap V3 的 AMM 曲线。首先,Uniswap V3 仍使用了 x * y = L^2x∗y=L2 的曲线,但是在 Uniswap V3 内存在区间流动性,最终造成了以下结果:

上图中的 virtual reserves就是 Uniswap V3 曲线,但实际上 real reserves才是真实情况。因为当价格位于  时,区间内只存在 y 资产;而当价格位于  时,区间内只存在 x 资产。我们需要建立价格  与  和  代币数量的关系。在后文内,我们使用  和  代表当前区间内  和  代币的数量。

Image

上图给出了更多的内容标识,以方便我们进行推导。其中靠近原点轴的曲线是实际代币构成的曲线,而远离原点的曲线则是在 AMM 计算过程中使用的 x \cdot y = L^2的曲线。推导如下:

x \cdot y = L^2 \quad (1)

(x_R + x_v)(y_R + y_v) = L^2 \quad (2)

此时,我们不知道 $x_v$和 $y_v$的数值,我们需要借助$x \cdot y = L^2$  和 $\frac{x}{y} = P$ 进行推导,可以获得如下等式:

x = \frac{L}{\sqrt{P}}

\quad y = \sqrt{P}

为了求解 $x_v$,我们计算当 $x_R = 0$的情况,此时 $P = P_B$

x_v(y_R + y_v) = x_v L \sqrt{P_B} = L^2

x_v = \frac{L}{\sqrt{P_B}}

同理,我们计算当 $y_R = 0$ 的情况,此时$P = P_A$ :

(x_R + x_v) y_v = L \sqrt{P_A} \cdot y_v = L^2

y_v = \frac{L}{\sqrt{P_A}}

最后,将上述计算获得 $x_v$和 $y_v$带入 $(x_R + x_v)(y_R + y_v) = L^2$可以获得如下结果:

\left(x_R + \frac{L}{\sqrt{P_B}}\right) \left(y_R + \frac{L}{\sqrt{P_A}}\right) = L^2

上述就是 AMM 曲线的最终形式,我们可以看到在此曲线内只使用了 $\sqrt{P}$ ,这也是为什么我们在上文最开始的 initialize函数内要求用户输入 sqrtPriceX96变量。接下来,我们需要推导在已知代币变化数量$\Delta_x$或者$\Delta_y$ 的情况下计算 $L$ 的数值。

我们首先计算 $P < P_A$ 的情况,此时区间内只存在数量为 $\Delta_x$的 x 资产,即$y_R = 0$ ,所以我们可以得到以下等式:

\left(\Delta_x + \frac{L}{\sqrt{P_B}}\right) L \sqrt{P_A} = L^2

\left(\Delta_x + \frac{L}{\sqrt{P_B}}\right) \sqrt{P_A} = L

\Delta_x = \frac{L}{\sqrt{P_A}} - \frac{L}{\sqrt{P_B}}

L = \frac{\Delta_x}{\frac{1}{\sqrt{P_A}} - \frac{1}{\sqrt{P_B}}}

然后,我们计算 $P > P_B$的情况,此时区间内只存在数量为 $\Delta_y$ 的 y 资产,即 $x_R = 0$,所以我们得到以下等式:

\frac{L}{\sqrt{P_B}} \left(\Delta_y + L \sqrt{P_A}\right) = L^2

\Delta_y = L \sqrt{P_B} - L \sqrt{P_A}

L = \frac{\Delta_y}{\sqrt{P_B} - \sqrt{P_A}}

最后,我们计算 $P_g < P < P_A$ 的情况,此时我们将该区间分为$(P_B, P)$$(P, P_A)$ 的情况。对于 $(P_B, P)$ 情况,我们可以视为$P > P_B$ 的情况,只不过此处的$P_B = P$。对于 $(P, P_A)$的情况,我们可以视为 $P < P_A$ 的情况,但是此处的 $P_B = P$。故而可以直接套用上述公式:

L = \frac{\triangle_x}{\frac{1}{\sqrt{P}} - \frac{1}{\sqrt{P_g}}} = \frac{\triangle_y}{\sqrt{P} - \sqrt{P_A}}

此等式可以用于添加流动性时的计算。在已知添加 x 代币数量的情况下,计算需要添加的 y 代币数量,反之也可以计算。

关于 $P_B < P < P_A$ 的情况,读者也可以使用下图理解:

我们继续提导我们的目标,即在已知 $L_2$ 的情况下,计算 $\Delta _x$$\Delta_y$。我们还是进行分情况讨论。

$P < P_A$ 时,我们可以看到:

$P > P_B$ 时,我们可以推导:

L_0 = \frac{y}{\sqrt{P_B} - \sqrt{P_A}}

L_1 = \frac{y + \triangle_y}{\sqrt{P_B} - \sqrt{P_A}}

\triangle_y = L_1 - L_0 = \frac{\triangle_y}{\sqrt{P_B} - \sqrt{P_A}}

$P_B < P < P_A$时,我们依旧是将原区间划分为两个区间来计算$\Delta_x$$\triangle_y$,如下:

完成上述公式推导后,我们可以最终完成 _modifyPosition 函数,即该函数的计算指定流动性所需要的代币数量的部分。首先将上述公式在 clamm/src/libraries/SqrtPriceMath.sol内实现,读者可以直接在 Uniswap 内直接将上述代码摘抄一下。此处的具体的实现读者可以自行参考相关代码。

此处我们可以讨论一下舍入问题,我们以 getAmount0Delta为例:

function getAmount0Delta(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, int128 liquidity)
    internal
    pure
    returns (int256 amount0)
{
    return liquidity < 0
        ? -getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false).toInt256()
        : getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true).toInt256();
}

此处的调用的 getAmount1Delta(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity, bool roundUp)的最后一个参数 roundUp 表示是否需要向上舍入。在此处,我们需要牢记一个原则,即所有误差都是由用户承担,流动性池不可以因为误差产生亏空。经过上文介绍,读者应该知道 getAmount0Delta用于计算用户在指定流动性和价格的情况下,用户所需要的代币数量,所以此处我们在liquidity >= 0的情况下,选择向上舍入,要求用户支付更多代币;而在 liquidity < 0的情况下,则放弃向上舍入,目标也是要求用户支付更多代币。

我们在 _modifyPosition内按照上述描述的情况进行实现。

function _modifyPosition(ModifyPositionParams memory params)
    private
    returns (Position.Info storage position, int256 amount0, int256 amount1)
{
    checkTicks(params.tickLower, params.tickUpper);
    Slot0 memory _slot0 = slot0;

    position = _updatePosition(params.owner, params.tickLower, params.tickUpper, params.liquidityDelta, _slot0.tick);

    if (params.liquidityDelta != 0) {
        if (_slot0.tick < params.tickLower) {
            amount0 = SqrtPriceMath.getAmount0Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        } elseif (_slot0.tick > params.tickUpper) {
            amount1 = SqrtPriceMath.getAmount1Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower),
                TickMath.getSqrtRatioAtTick(params.tickUpper),
                params.liquidityDelta
            );
        } else {
            uint128 liquidityBefore = liquidity;

            amount0 = SqrtPriceMath.getAmount0Delta(
                _slot0.sqrtPriceX96, TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta
            );
            amount1 = SqrtPriceMath.getAmount1Delta(
                TickMath.getSqrtRatioAtTick(params.tickLower), _slot0.sqrtPriceX96, params.liquidityDelta
            );
            liquidity = params.liquidityDelta < 0
                ? liquidityBefore - uint128(-params.liquidityDelta)
                : liquidityBefore + uint128(params.liquidityDelta);
        }
    }
}

在此处,如果添加流动性的区间包括 _slot0.tick,此处会对 liquidity进行修改。此处的 liquidity指在当前价格下生效的流动性数量。

至此,我们就完成了流动性提供的所有流程。在此流程内,我们以此使用了以下函数:

  1. _modifyPosition用来修改流动性提供区间的参数并计算指定流动性提供所需要的代币数量

  2. _updatePosition_modifyPosition内部更新价格区间,目前我们实现了更新区间上限和下限 tick 的相关功能

标签:流动性,tickUpper,liquidityDelta,V3,params,Uniswap,tick,我们,tickLower
From: https://blog.csdn.net/openbuild/article/details/145267482

相关文章

  • CF div3 998(F,G)
    F\(dp\)+组合数学需要注意,数组中\(>1\)的数字个数不会超过\(log_{2}k\)个。先暂时不考虑\(1\)的摆放,只考虑所有\(>1\)的数:设\(f_{l,i}:\)长度为\(l\),乘积为\(i\),且所有元素均\(>1\)的数组个数考虑数组的最后一个元素\(d\),必有\(d|i\)成立,因为每个元素一定是\(i\)的因子。则......
  • 多平台批量无水印视频图集下载器 DyD v3.0.3 免费版
    好久没有给大家推荐去水印的软件了,今天带来的这款DyD下载助手对自媒体小伙伴非常有帮助。它不仅能对视频进行编辑修改,帮助你打造原创内容,还自带了不少实用工具,提升你的创作效率。支持抖音、快手、小红书等平台。如今短视频非常火爆,任何人都能用手机拍摄一段视频。有了这个工具......
  • 远铂DIY官网系统AI助手新增支持DeepSeek-V3 AI模型接口
            近日,量化巨头幻方量化的子公司深度求索(DeepSeek)发布了全新系列模型DeepSeek-V3,并同步开源。这一事件迅速引爆AI圈,DeepSeek-V3不仅霸榜开源模型,更在性能上与全球顶尖闭源模型GPT-4o和Claude-3.5-Sonnet不相上下。    更令人瞩目的是,该模型的训练成本仅......
  • 计算 SAMOut V3 在将词汇表从1万 增加到6千万的情况下能够减少多少参数
    当我们将词汇表从60,000,000(六千万)减少到10,000时,实际上是在缩小模型的词嵌入层及其共享的语言模型头(LMHead)的规模。这将导致参数量显著减少。我们可以通过以下步骤来计算具体的参数减少量。参数量减少计算假设条件:原词汇表大小......
  • 云消息队列 Kafka 版 V3 系列荣获信通院“云原生技术创新标杆案例”
    2024年12月24日,由中国信息通信研究院(以下简称“中国信通院”)主办的“2025中国信通院深度观察报告会:算力互联网分论坛”,在北京隆重召开。本次论坛以“算力互联网新质生产力”为主题,全面展示中国信通院在算力互联网产业领域的研究、实践与业界共识,与产业先行者共同探索算力互......
  • DeepSeek-V3 的 MoE 架构解析:细粒度专家与高效模型扩展
    DeepSeek-V3采用的DeepSeekMoE架构,通过细粒度专家、共享专家和Top-K路由策略,实现了模型容量的高效扩展。每个MoE层包含1个共享专家和256个路由专家,每个Token选择8个路由专家,最多路由至4个节点。这种稀疏激活的机制,使得DeepSeek-V3能够在不显著增加计算成本的......
  • DeepSeek V3:AI 模型的游戏规则改变者
    DeepSeekV3:AI模型的游戏规则改变者什么是DeepSeekV3?DeepSeekV3是一款具有革命性的混合专家(MoE)模型,总参数达6710亿,每个标记激活370亿参数。MoE方法允许多个专门模型(即“专家”)在门控网络下协同工作,门控网络为每个输入选择最佳“专家”。这实现了高效推理和具有成本效益的训......
  • DeepSeek推出搭载V3大模型App:开启智能应用新时代 
     科技浪潮下的创新突破 在当今科技飞速发展的时代,人工智能(AI)无疑是最为耀眼的领域之一。从早期简单的算法模型,到如今能够模拟人类思维、进行复杂任务处理的大型语言模型,AI的进化历程令人瞩目。而在这一进程中,DeepSeek推出的搭载V3大模型的App,宛如一颗璀璨的新星,为智能应用......
  • 3DDFA-V3——基于人脸分割几何信息指导下的三维人脸重建
    3DDFA-V3——基于人脸分割几何信息指导下的三维人脸重建1.研究背景从二维图像中重建三维人脸是计算机视觉研究的一项关键任务。在虚拟现实、医疗美容、计算机生成图像等领域中,研究人员通常依赖三维可变形模型(3DMM)进行人脸重建,以定位面部特征和捕捉表情。然而,现有的方法往......
  • 【花雕学编程】Arduino动手做(246)---ESP8266 NodeMCU V3 Web Server
    37款传感器与执行器的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里准备逐一动手尝试系列实验,不管成功(程序走通)与否,都会记录下来——小小的......