Cracking C++(7): 使用 fp16 类型

1. 目的

fp16 类型主要作用是计算加速, 在使用 CPU 执行计算的平台上, x86 硬件并没有原生支持 fp16, 但是网络上可以找到模拟实现, 可以初步体验下 fp16 类型的基本计算。

对于 fp16 相关的 SIMD 计算, 本文没有涉及。

2. 支持 fp16 的平台

fp16 指的是 IEEE754-2008 提出的格式。

在 x86 CPU 上,不能完全支持 fp16:

  • 如果只是开启 SSE2 的编译支持, 那么是用 float 对应的指令来模拟的
  • 如果开启了 -mavx512fp16 编译选项, 并且硬件支持, 那么将生成 AVX512-FP16 指令

在 arm CPU 上, arm8.2 架构提供了 fp16 对应的硬件指令, 最常见的设备就是手机了。

NNIDIA GPU 和 AMD GPU 的 fp16 支持暂时不了解。

3. fp16 的模拟实现

3.1 开源库概况

在日常开发场景下, 一边写代码一边运行程序, 使用到 fp16 类型并且希望结果是正确的, 有这几种选择:

  • 使用带 avx512 的 CPU 设备
  • 使用 Android / iPhone 手机
  • 使用 NVIDIA / AMD GPU
  • 使用模拟实现的 fp16 库

其中前三种方式有一定门槛, 使用 fp16 模拟库则容易得多。了解到的 fp16 模拟库:

  • half (参考[1]) , C++ 实现, 模拟 fp16 类型和相关计算
  • FP16 (参考[2]), C 实现, 包括数据类型转换, 并和其他模拟实现做了性能评测比较

直观体验是, 这两个库里面没使用 __fp16 关键字, 显然只是模拟实现。

如果写好的程序打算迁移到支持 fp16 的设备上运行, 用上述两个模拟库无法享受硬件相关的 SIMD 指令加速。更合适的 fp16 C/C++ 库应当是:

  • 在支持 fp16 的硬件 + 编译参数组合下, 使用 fp16 类型相关的 builtin 函数
  • 在不支持 fp16 的硬件 + 编译参数组合下, 使用模拟实现
    • 在支持 SSE2 的平台上, 可以用 SSE2 的 SIMD 指令来模拟 fp16 的指令, 至少比按标量计算要快。参考[3]
    • 对于标量的操作, 完全是软件模拟
  • 提供 C++ class 的接口, 并在硬件 + 编译参数支持 fp16 时, 提供转换到原生 fp16 类型及其指针类型的 operator 转换函数

不过, 从头实现是耗时且困难的, 参考 half 和 FP16 两个库的实现则可以提供适当加速, 原生 fp16 类型的使用则可参照 ncnn 的代码。

最近火起来的 llama.cpp 和 whisper.cpp, 都是基于 ggml, 而 ggml 在不支持 fp16 的平台上, 使用了 FP16 项目中的代码作为模拟实现。

3.2 x86平台的编译器对 fp16 类型的支持

Clang 3.5 开始提供 __fp16 类型。 也就是说在 x64 的 PC 上,可以使用 __fp16. Clang 15.0 支持了 _Float16 类型。

GCC 的 x86 平台则不支持 __fp16, GCC 12.1 开始支持了 _Float16 类型。

MSVC 没有支持 __fp16_Float16.

3.3 __fp16 类型的限制:不能作为函数参数

__fp16 是一个纯粹的存储类型, 不能按值传参, 因为只有 API permissive 类型(https://gitlab.com/x86-psABIs/x86-64-ABI)才可以按值传入。

void print(FP16 x)  // 这里会编译报错,error: parameters cannot have __fp16 type; did you forget * ?
{
    //std::cout << "result " << (float)x << "\n";
    std::cout << "result " << fmt::format("{}", (float)x) << "\n";
}

参考: [4].

3.4 封装 half 库

这里的想法是, 提供一个 class, 在支持 __fp16 的平台上, 数据成员是 __fp16 类型, 否则使用 half 库的 half_float::half 类型。

// https://github.com/pytorch/glow/issues/1329

#include <fmt/core.h>
#include <iostream>
#include <stdint.h>
#include <bitset>

#include "half.hpp"

// _Float16
// GCC >= 12.1
// Clang >= 15.0

// __fp16
// GCC: 13.1 still N/A
// Clang >= 3.5

#define PHANTOM_CHECK_VERSION(wanted_major, wanted_minor, wanted_patch, current_major, current_minor, current_patch) \
                (((current_major) > (wanted_major)) || \
                    (((current_major) == (wanted_major)) \
                        && (((current_minor) > (wanted_minor)) || \
                            (((current_minor) == (wanted_minor)) \
                                && ((current_patch) >= (wanted_patch))))))

#define PHANTOM_CHECK_GCC_VERSION(wanted_major, wanted_minor, wanted_patch) \
                PHANTOM_CHECK_VERSION(wanted_major, wanted_minor, wanted_patch, \
                                      __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)

#define PHANTOM_CHECK_CLANG_VERSION(wanted_major, wanted_minor, wanted_patch) \
                PHANTOM_CHECK_VERSION(wanted_major, wanted_minor, wanted_patch, \
                                      __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)


#ifdef __clang__
#define PHANTOM_CLANG_COMPILER 1
#elif defined(__INTEL_COMPILER)
#define PHANTOM_INTEL_COMPILER 1
#elif defined(__MINGW64__)
#define PHANTOM_MINGW64_COMPILER 1
#elif defined(__MINGW32__)
#define PHANTOM_MINGW32_COMPILER 1
#elif defined(__GCC__)
#define PHANTOM_GCC_COMPILER 1
#elif defined(_MSC_VER)
#define PHANTOM_MSVC_COMPILER 1
#endif

#if PHANTOM_CLANG_COMPILER && PHANTOM_CHECK_CLANG_VERSION(15, 0, 0)
#define fp16_storage_type _Float16
#elif PHANTOM_CLANG_COMPILER && PHANTOM_CHECK_CLANG_VERSION(3, 5, 0)
#define fp16_storage_type __fp16
#elif PHANTOM_GCC_COMPILER && PHANTOM_CHECK_GCC_VERSION(12, 1, 0)
#define fp16_storage_type _Float16
#else
#define fp16_storage_type half_float::half data_;
#endif

// https://github.com/pytorch/glow/issues/1329
struct FP16
{
    fp16_storage_type data_;
    FP16(float x = 0.)
        : data_(x)
    {
    }
    FP16 operator+(const FP16&) const;
    operator float() const
    {
        return data_;
    }
};

FP16 FP16::operator+(const FP16& c) const
{
    FP16 result;
    result.data_ = (this->data_ + c.data_);
    return result;
}

void print(FP16 x)
{
    //std::cout << "result " << (float)x << "\n";
    std::cout << "result " << fmt::format("{}", (float)x) << "\n";
}

3.5 执行计算

使用前一步封装好的 FP16 类, 执行标量、 数组的计算。

int main()
{
    FP16 pi(3.1415926f);
    FP16 one(1.0f);
    FP16 res = pi + one;

    print(pi);
    print(one);
    print(res);

    FP16 data[4] = { pi, pi, pi, pi };
    for (int i = 0; i < 4; i++)
    {
        data[i] = data[i] + one;
    }
    for (int i = 0; i < 4; i++)
    {
        print(data[i]);
    }

    return 0;
}

输出

result 3.140625
result 1
result 4.140625
result 4.140625
result 4.140625
result 4.140625
result 4.140625

参考了 half库的文档([5]) https://half.sourceforge.net/index.html

4. Referennces

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cracking the PM Interview: How to Land a Product Manager Job in Technology》是一本出色的产品经理求职指南。它由Gayle Laakmann McDowell和Jackie Bavaro联合撰写,对于想在科技行业中担任产品经理职位的人来说是一本非常有用的资源。 该书涵盖了产品经理职位的各个方面,帮助读者了解这个职位的基本知识和技能要求。它提供了关于面试准备的建议,包括如何构建个人简历,编写求职信和答题技巧。此外,书中还介绍了常见的面试题目和解答方法,让读者可以更好地应对具体的面试环节。 这本书的另一个亮点是它提供了大量的实用技巧和建议,帮助读者掌握产品管理的关键要素。它涵盖了市场分析、需求收集、项目管理和团队合作等重要主题,并提供了许多行业成功人士的经验分享。通过学习这些内容,读者可以更好地了解产品经理的职责和职业发展的道路。 此外,书中还包括了一些有关技术和数据分析的内容,因为在当今科技行业中,产品经理必须具备与工程师和设计师有效沟通的能力。通过掌握这些技术和数据分析的概念,读者可以提升自己的竞争力,并在面试中表现出色。 总之,《Cracking the PM Interview: How to Land a Product Manager Job in Technology》是一本详尽而实用的产品经理求职指南,它提供了全面的知识和技能要求,帮助读者成功获得科技行业产品经理职位。无论是初入行的新手还是有经验的专业人士,都能从中获益良多。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值