Quantcast
Channel: 英特尔开发人员专区文章
Viewing all 154 articles
Browse latest View live

英特尔® Software Guard Extensions 教程系列: 第四部分,安全区设计

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)教程系列第四部分将介绍如何设计安全区及其界面。 我们将回顾第三部分中定义的安全区边界,并识别必要的桥接函数,检查桥接函数对对象模型的影响,并创建所需的项目基础设施以将安全区集成至应用。 本部分仅为安全区 ECALLS 提供桩文件;第五部分将介绍完全集成安全区。

文章英特尔® Software Guard Extensions 教程系列简介列举了所有已经发布的教程。

该系列的安装部分将提供源代码:安全区桩文件和界面功能可供下载。

应用架构

开始设计安全区界面之前,我们首先需要了解整体应用架构。 如第一部分所述,安全区以动态加载库(Windows* 下的 DLL 和 Linux* 下的共享库)的形式实施,而且只能链接 100% 原生 C 代码。

不过 Tutorial Password Manager 有一个用 C# 编写的 GUI。 它使用用 C++/CLI 编写的混合模式汇编程序帮助我们从托管代码移至非托管代码,不过该汇编程序包含的原生代码不是 100% 原生模块,而且无法直接与英特尔 SGX 安全区交互。 尝试将非信任安全区桥接函数融入 C++/CLI 汇编程序会造成致命错误:

	Command line error D8045: cannot compile C file 'Enclave_u.c' with the /clr option

这表示我们需要将非信任桥接函数放在全是原生代码的独立 DLL 中。 因此,应用最少需要三个 DLL:C++/CLI 内核,安全区桥接和安全区。 其结构如图 1 所示。


图 1. 带有安全区的混合模式应用组成部分。

进一步完善

由于安全区桥接函数必须驻留在单独的 DLL 中,因此我们执行下一步骤,将直接处理安全区的所有功能都放置在同一个 DLL 中。 划分应用层不仅能够简化程序管理(和调试),还能减少对其他模块的影响,从而简化集成过程。 当类或模块的特定任务包含清晰定义的边界,对其他模块的修改不太可能对其造成影响。

在这种情况下,PasswordManagerCoreNative类不应该承担其他实例化安全区的任务。 它只需知道平台是否支持英特尔 SGX,以便执行相应的功能。

例如,以下代码块显示了 unlock()方法:

int PasswordManagerCoreNative::vault_unlock(const LPWSTR wpassphrase)
{
	int rv;
	UINT16 size;

	char *mbpassphrase = tombs(wpassphrase, -1, &size);
	if (mbpassphrase == NULL) return NL_STATUS_ALLOC;

	rv= vault.unlock(mbpassphrase);

	SecureZeroMemory(mbpassphrase, size);
	delete[] mbpassphrase;

	return rv;
}

这种方法能够轻松地将用户的口令当作 wchar_t,将其转化成可变长度编码 (UTF-8),然后在仓库对象中调用 unlock()方法。 为了防止安全区处理函数和逻辑使该类和该方法变得散乱,最好通过添加一行代码为该方法添加安全区支持:

int PasswordManagerCoreNative::vault_unlock(const LPWSTR wpassphrase)
{
	int rv;
	UINT16 size;

	char *mbpassphrase = tombs(wpassphrase, -1, &size);
	if (mbpassphrase == NULL) return NL_STATUS_ALLOC;

	// Call the enclave bridge function if we support Intel SGX
	if (supports_sgx()) rv = ew_unlock(mbpassphrase);
	else rv= vault.unlock(mbpassphrase);

	SecureZeroMemory(mbpassphrase, size);
	delete[] mbpassphrase;

	return rv;
}

我们的目标是,如果可行,尽量减少该类对安全区的感知。 PasswordManagerCoreNative类另外只需添加一个标记,以帮助设置并获取英特尔 SGX 支持和方法。

class PASSWORDMANAGERCORE_API PasswordManagerCoreNative
{
	int _supports_sgx;

	// Other class members ommitted for clarity

protected:
	void set_sgx_support(void) { _supports_sgx = 1; }
	int supports_sgx(void) { return _supports_sgx; }

设计安全区

整体应用计划就绪后,现在可以开始设计安全区及其界面。 为此,我们回顾一下第三部分图 2 中面向应用内核的类图。 将驻留在安全区内的对象涂成绿色,非信任组件涂成蓝色。


图 2. 面向基于英特尔® Software Guard Extensions 的 Tutorial Password Manager 的类图。

该安全区边界仅穿过了一个连接:PasswordManagerCoreNative对象和 Vault对象之间的链路。 这表示大部分 ECALL 将仅成为围绕 Vault中的类方法的包装程序。 我们还需要添加其他 ECALL 来管理安全区基础设施。 开发安全区所产生的一个问题是,ECALL、OCALL 和桥接函数必须为原生 C 代码,而且我们要充分利用 C++ 特性。 安全区启动后,我们还需要能够跨越 C 与 C++ 之间的障碍的函数(对象、构造函数、过载等)。

包装程序和桥接函数将前往它们自己的 DLL,我们将其命名为 EnclaveBridge.dll。 为了清楚起见,我们分别为包装程序函数和进行 ECALL 的桥接函数加上前缀 ew_(针对 “enclave wrapper”)和 ve_(针对 “vault enclave”)。

PasswordManagerCoreNativeVault中相应方法的调用将遵循图 3 所示的基本流程。


图 3. 桥接函数和 ECALL 执行流程。

PasswordManagerCoreNative中的方法将调用至 PasswordManagerCoreNative中的包装程序函数。 该包装程序将反过来调用一个或多个 ECALL,以进入安全区并调用 Vault对象中相应的类方法。 完成所有 ECALL 后,包装程序函数返回至 PasswordManagerCoreNative中的调用方法,并为其提供一个返回值。

安全区逻辑

设计安全区的第一步是制定安全区管理系统。 安全区必须启动,而且生成的安全区 ID 必须提供给 ECALL。 理想状态下,该过程对应用的所有上层都是透明的。

对 Tutorial Password Manager 来说,最简单的解决方法是使用 EnclaveBridge DLL 中的全局变量保留安全区信息。 这种设计决策有一个限制条件:安全区中每次只有一个线程处于活跃状态。 不过这种方法比较合理,因为仓库上运行多个线程对密码管理器应用没有任何帮助。 大部分操作由用户界面驱动完成,不会消耗太长的 CPU 时间。

为解决透明度问题,包装程序函数会首先调用函数以查看安全区是否启动,如果没有,将启动该安全区。这种逻辑非常简单:

#define ENCLAVE_FILE _T("Enclave.signed.dll")

static sgx_enclave_id_t enclaveId = 0;
static sgx_launch_token_t launch_token = { 0 };
static int updated= 0;
static int launched = 0;
static sgx_status_t sgx_status= SGX_SUCCESS;

// Ensure the enclave has been created/launched.

static int get_enclave(sgx_enclave_id_t *eid)
{
	if (launched) return 1;
	else return create_enclave(eid);
}

static int create_enclave(sgx_enclave_id_t *eid)
{
	sgx_status = sgx_create_enclave(ENCLAVE_FILE, SGX_DEBUG_FLAG, &launch_token, &updated, &enclaveId, NULL);
	if (sgx_status == SGX_SUCCESS) {
		if ( eid != NULL ) *eid = enclaveId;
		launched = 1;
		return 1;
	}

	return 0;
}

包装程序函数首先调用 get_enclave(),通过检查静态变量查看安全区是否已启动。 如果已经启动,(可选)将安全区 ID 填充至 eid指示器。 这一步骤是可选的,因为安全区 ID 也能够以全局变量 enclaveID的形式保存,可以直接使用。

如果出现断电或系统因为漏洞而出现崩溃,导致安全区丢失怎么办? 如果出现这种情况,我们可以检查 ECALL 的返回值:它表示 ECALL 操作本身(而不是安全区中调用的函数)成功与否。

sgx_status = ve_initialize(enclaveId, &vault_rv);

在安全区中调用的函数的返回值(如有)将通过以次要参数形式提供的指示器传输至 ECALL(Edger8r 工具自动为您生成这些函数原型)。 必须时刻检查 ECALL 的返回值。 如果结果不是 SGX_SUCCESS,表示程序没有成功进入安全区,所请求的函数也没有运行。 (请注意,我们将 gx_status同样定义为全局变量。 这是另外一种简化方法,源于单线程设计。)

我们将添加以下函数以检查 ECALL 返回的错误,并查看丢失或崩溃的安全区:

static int lost_enclave()
{
	if (sgx_status == SGX_ERROR_ENCLAVE_LOST || sgx_status == SGX_ERROR_ENCLAVE_CRASHED) {
		launched = 0;
		return 1;
	}

	return 0;
}

这些错误是可恢复的。 上层目前没有处理这些特定情况的逻辑,不过我们在 EnclaveBridge DLL 中提供该逻辑,以支持未来的增强措施。

大家还需注意,这里不提供破坏安全区的函数。 只要用户打开密码管理器应用,安全区就将处于就绪状态,即使选择锁定仓库也是如此。 安全区的这一规范并不合理。 即使处于闲置状态,安全区也可提取有限资源池。 本教程系列的后续部分探讨数据密封时,我们再来尝试解决这一问题。

安全区定义语言

开始实际设计安全区之前,我们简要介绍一下安全区定义语言 (EDL) 语法。 安全区的桥接函数(ECALL 和 OCALL)均在其 EDL 文件中构建原型,其通用结构如下所示:

enclave {
	// Include files

	// Import other edl files

	// Data structure declarations to be used as parameters of the function prototypes in edl

	trusted {
	// Include file if any. It will be inserted in the trusted header file (enclave_t.h)

	// Trusted function prototypes (ECALLs)

	};

	untrusted {
	// Include file if any. It will be inserted in the untrusted header file (enclave_u.h)

	// Untrusted function prototypes (OCALLs)

	};
};

ECALL 在可信区构建原型,OCALL 在非信任区构建原型。

EDL 语法与 C 类似,而且其函数原型也与 C 函数原型非常相似,但并不相同。 具体而言,桥接函数参数和返回值会受到部分基本数据类型的限制,而且 DL 包含部分定义某种安全区行为的附加密码和语法。 英特尔® Software Guard Extensions(英特尔® SGX) SDK用户指南详细介绍了 EDL 语法,并包含有关如何创建示例安全区的教程。 此处我们不再重复,只介绍特定于本应用的一些语言元素。

参数传递至安全区函数后,将封送至安全区的受保护内存空间。 不需要针对以数值形式传递的参数执行特殊操作,因为这些值放在安全区的受保护堆栈上,与用于其他函数调用情况一样。 而指示器则完全不同。

对于以指示器形式传递的参数来说,指示器引用的数据必须封送至安全区内,或从安全区封送出来。 执行此数据封送的边缘例程需要了解两种情况:

  1. 朝哪个方向拷贝数据:拷贝至桥接函数,拷贝出桥接函数,或这两者?
  2. 指示器引用的数据缓冲区的大小?

指示器方向

向函数提供指示器参数时,必须在括号中用关键明方向: [in][out][in, out]。 相关含义如表 1 所示。

方向       

ECALL

OCALL

in

缓冲区从应用拷贝至安全区。 更改仅影响安全区内的缓冲区。

缓冲区从安全区拷贝至应用。 更改仅影响安全区外的缓冲区。

out

缓冲区在安全区内分配并初始化为零。 ECALL 退出时拷贝至原始缓冲区。

缓冲区在安全区外分配并初始化为零。 OCALL 退出时非信任缓冲区拷贝至安全区内的原始缓冲区。

in, out               

数据来回拷贝。

与 ECALL 相同。

表 1. 指示器方向参数及其在 ECALL 和 OCALL 中的含义。

通过该表格我们注意到,方向与所调用的桥接函数有关。 对 ECALL 而言,[in]表示“将缓冲区拷贝至安全区”,而对 OCALL 而言。它表示“将缓冲区拷贝至非信任函数”。

(还有一个名为 user_check的选项可用于此处,但与我们此处介绍的内容无关。 关于其用法和用途,请参阅 SDK 文档。)

缓冲区大小

边缘例程负责计算缓冲区大小(单位:字节),计算公式为:

bytes = element_size * element_count

默认情况下,边缘例程假定 element_count = 1,并通过指示器参数引用的元素计算 element_size,例如,对整数指示器来说,它假定 element_size为:

sizeof(int)

对数据类型固定的单个元素(比如 intfloat)来说,该函数的 EDL 原型中不需要提供其他信息。 对 void指示器来说,必须指定元素大小,否则在编译时会出现错误。 对数据缓冲区长度大于一个元素的阵列、charwchar_t字符串或其他类型来说,必须指定缓冲区中的元素数量,否则仅拷贝一个元素。

根据需要在括号中的指示器关键字中添加 countsize参数(或两者)。 它们可设为恒定值,或该函数的其中一个参数。 在多数情况下,countsize的功能相同,但最好在准确的环境中使用。 严格来说,传递 void 指示器时,仅指定 size。 其他情况需使用 count

如果传递 C 字符串或 wstring(NULL 终止的 charwchar_t阵列),可以在 countsize位置使用 stringwstring参数。 在这种情况下,边缘例程将直接获取字符串的长度,以确定缓冲区大小。

function([in, size=12] void *param);
function([in, count=len] char *buffer, uint32_t len);
function([in, string] char *cstr);

注意,如果方向设置为 [in][in, out],那么只能使用 stringwstring。 如果方向仅设置为 [out],而尚未创建字符串,那么边缘例程将不知道缓冲区的大小。 指定 [out, string]会导致编译期间出现错误。

包装程序和桥接函数

现在我们已经准备好定义包装程序和桥接函数。 如前所述,大部分 ECALL 将成为与 Vault中的类方法有关的包装程序函数。 公共成员函数的类定义如下所示:

class PASSWORDMANAGERCORE_API Vault
{
	// Non-public methods and members ommitted for brevity

public:
	Vault();
	~Vault();

	int initialize();
	int initialize(const char *header, UINT16 size);
	int load_vault(const char *edata);

	int get_header(unsigned char *header, UINT16 *size);
	int get_vault(unsigned char *edate, UINT32 *size);

	UINT32 get_db_size();

	void lock();
	int unlock(const char *password);

	int set_master_password(const char *password);
	int change_master_password(const char *oldpass, const char *newpass);

	int accounts_get_count(UINT32 *count);
	int accounts_get_info(UINT32 idx, char *mbname, UINT16 *mbname_len, char *mblogin, UINT16 *mblogin_len, char *mburl, UINT16 *mburl_len);

	int accounts_get_password(UINT32 idx, char **mbpass, UINT16 *mbpass_len);

	int accounts_set_info(UINT32 idx, const char *mbname, UINT16 mbname_len, const char *mblogin, UINT16 mblogin_len, const char *mburl, UINT16 mburl_len);
	int accounts_set_password(UINT32 idx, const char *mbpass, UINT16 mbpass_len);

	int accounts_generate_password(UINT16 length, UINT16 pwflags, char *cpass);

	int is_valid() { return _VST_IS_VALID(state); }
	int is_locked() { return ((state&_VST_LOCKED) == _VST_LOCKED) ? 1 : 0; }
};

此类中包含多个问题函数。 部分问题函数显而易见,比如构造函数、析构函数和 initialize()过载。 使用 C 函数时必须调用这些 C++ 特性。 而还有一些问题函数并不明显,因为它们是函数的内在设计所造成的。 (一些问题方法故意设计不当,以便我们在本教程中解决这些具体问题,而另外一些就是设计不当!) 我们将逐一解决这些问题,从而展示面向包装程序函数的原型和面向代理/桥接例程的 EDL 原型。

构造函数和析构函数

在非英特尔 SGX 代码路径中,Vault 类是 PasswordManagerCoreNative 的一个成员。 我们在英特尔 SGX 代码路径中无法进行此操作;但安全区能够包含 C++ 代码,只要桥接函数本身是单纯的 C 函数。

由于我们已将安全区限制在单个线程内,因此在安全区内能够将 Vault类变成静态的全局对象。 这样大大简化了代码,而且对其进行实例化时无需创建桥接函数和逻辑。

initialize() 过载

有两种原型可用于 initialize()方法:

  1. 不包含参数的方法针对无任何内容的新密码仓库初始化 Vault 对象。 该密码仓库为用户首次创建。
  2. 包含两个参数的方法通过仓库文件的头文件初始化对象。 它表示用户正在打开现有密码仓库(并且稍后尝试解锁)。

该过程将分成两个包装程序函数:

ENCLAVEBRIDGE_API int ew_initialize();
ENCLAVEBRIDGE_API int ew_initialize_from_header(const char *header, uint16_t hsize);

而且相应的 ECALL 将定义为:

public int ve_initialize ();
public int ve_initialize_from_header ([in, count=len] unsigned char *header, uint16_t len);

get_header()

该方法存在一个基本设计问题。 原型如下:

int get_header(unsigned char *header, uint16_t *size);

该函数可完成下列两个任务中的一个:

  1. 获取面向仓库文件的头文件数据块,并将其放在按照头文件指向的缓冲区中。 调用者必须分配足够的内存来保存此数据。
  2. 如果在头文件参数中传递 NULL 指示器,按照大小指向的 uint16_t将设置为头文件数据块的大小,以便调用者知道分配多少内存。

在部分编程周期中,它是一种常见的压缩技术,但会导致安全区遇到一个问题:将指示器传递至 ECALL 或 OCALL 时,边缘函数会将指示器引用的数据拷贝至安全区内,或从安全区拷贝出来(或两者)。 这些边缘函数需要知道数据缓冲区的大小,以便知道拷贝多少字节。 第一种用法涉及一个有效参数,其大小可变(这不是问题),而第二种用法包含一个 NULL 指示器,其大小为零。

我们可以为该 ECALL 构建一个 EDL 原型以使其运行,但我们应该确保其清晰度(而非简洁性)。 最好将其分成两个 ECALL:

public int ve_get_header_size ([out] uint16_t *sz);
public int ve_get_header ([out, count=len] unsigned char *header, uint16_t len);

安全区包装程序函数会负责所需的逻辑,因此我们无需更改其他的类:

ENCLAVEBRIDGE_API int ew_get_header(unsigned char *header, uint16_t *size)
{
	int vault_rv;

	if (!get_enclave(NULL)) return NL_STATUS_SGXERROR;

	if ( header == NULL ) sgx_status = ve_get_header_size(enclaveId, &vault_rv, size);
	else sgx_status = ve_get_header(enclaveId, &vault_rv, header, *size);

	RETURN_SGXERROR_OR(vault_rv);
}

accounts_get_info()

这种方法的运行方式与 get_header()类似:传递 NULL 指示器,并返回相应参数中对象的大小。 不过,由于有多个参数,它看起来不太美观,而且比较凌乱。 最好将其分成两个包装程序函数:

ENCLAVEBRIDGE_API int ew_accounts_get_info_sizes(uint32_t idx, uint16_t *mbname_sz, uint16_t *mblogin_sz, uint16_t *mburl_sz);
ENCLAVEBRIDGE_API int ew_accounts_get_info(uint32_t idx, char *mbname, uint16_t mbname_sz, char *mblogin, uint16_t mblogin_sz, char *mburl, uint16_t mburl_sz);

以及两个相应的 ECALL:

public int ve_accounts_get_info_sizes (uint32_t idx, [out] uint16_t *mbname_sz, [out] uint16_t *mblogin_sz, [out] uint16_t *mburl_sz);
public int ve_accounts_get_info (uint32_t idx,
	[out, count=mbname_sz] char *mbname, uint16_t mbname_sz,
	[out, count=mblogin_sz] char *mblogin, uint16_t mblogin_sz,
	[out, count=mburl_sz] char *mburl, uint16_t mburl_sz
);

accounts_get_password()

这是最糟糕的情况。 原型如下:

int accounts_get_password(UINT32 idx, char **mbpass, UINT16 *mbpass_len);

大家首先会看到,它将指示器传递至 mbpass中的指示器。 该方法正在分配内存。

一般来说,这种设计不太好。 Vault类中没有其他方法分配内存,因此它内部是不一致的,而且 API 打破了常规,因为它不提供方法以代表调用者释放内存。 还会导致安全区遇到一个特殊的问题:安全区无法在非信任空间中分配内存。

我们可以在包装程序函数中解决该问题。 它能够分配内存,然后形成 ECALL,这样对调用者来说一切都是透明的。但不管怎样我们必须在 Vault类中修改该方法,以便只将其改成正确的方法,并对 PasswordManagerCoreNative进行相应修改。 应该为调用者提供两个函数:一个用于获取密码长度,另一个用于提取密码(与之前的两个示例相同)。 PasswordManagerCoreNative应负责分配内存,而非这些函数(非英特尔 SGX 代码路径也应更改)。

ENCLAVEBRIDGE_API int ew_accounts_get_password_size(uint32_t idx, uint16_t *len);
ENCLAVEBRIDGE_API int ew_accounts_get_password(uint32_t idx, char *mbpass, uint16_t len);

现在,EDL 定义如下所示:

public int ve_accounts_get_password_size (uint32_t idx, [out] uint16_t *mbpass_sz);
public int ve_accounts_get_password (uint32_t idx, [out, count=mbpass_sz] char *mbpass, uint16_t mbpass_sz);

load_vault()

load_vault()问题非常微妙。 原型非常简单,咋一看没有任何问题:

int load_vault(const char *edata);

该方法负责将已完成加密和序列化的密码数据库加载至 Vault对象。 由于 Vault对象已经读取了头文件,因此它知道入站缓冲区的大小。

此处的问题是,安全区的边缘函数没有这类信息。 必须为 ECALL 明确提供长度,以便边缘函数知道将多少字节的数据从入站缓冲区拷贝至安全区的内部缓冲区,但大小保存在安全区中, 边缘函数无法使用这类信息。

包装程序函数的原型能够镜像该类方法的原型,如下所示:

ENCLAVEBRIDGE_API int ew_load_vault(const unsigned char *edata);

但 ECALL 需要以参数形式传递头文件大小,以便在 EDL 文件中定义入站数据缓冲区的大小:

public int ve_load_vault ([in, count=len] unsigned char *edata, uint32_t len)

为确保此操作对调用者的透明度,我们为包装程序函数提供额外的逻辑。 它将负责从安全区提取仓库大小,然后以参数形式将其传递至 ECALL。

ENCLAVEBRIDGE_API int ew_load_vault(const unsigned char *edata)
{
	int vault_rv;
	uint32_t dbsize;

	if (!get_enclave(NULL)) return NL_STATUS_SGXERROR;

	// We need to get the size of the password database before entering the enclave
	// to send the encrypted blob.

	sgx_status = ve_get_db_size(enclaveId, &dbsize);
	if (sgx_status == SGX_SUCCESS) {
		// Now we can send the encrypted vault data across.

		sgx_status = ve_load_vault(enclaveId, &vault_rv, (unsigned char *) edata, dbsize);
	}

	RETURN_SGXERROR_OR(vault_rv);
}

浅谈 Unicode

第三部分中,我们提到过 PasswordManagerCoreNative类同样承担在 wchar_tchar字符串之间进行转换的任务。 倘若安全区支持 wchar_t数据类型,那为何要进行此操作?

这种设计决策旨在最大限度地缩小我们的占地空间。 在 Windows 中,wchar_t数据类型是面向 Win32 API 的原生编码,负责保存 UTF-16编码字符。 在 UTF-16 中,每个字符都是 16 位,以便支持非 ASCII 字符,尤其是不以拉丁字母表为基础或字符数量较多的语言。 UTF-16 的问题是字符始终保持 16 位的长度,即使在编码纯 ASCII 文本时也是如此。

在用户帐号信息为纯 ASCII 的常见情况下,将两倍多的数据保存在磁盘上或安全区内,当需要拷贝并加密这些额外字节时,会使性能会受到影响,相比之下,Tutorial Password Manager 选择将所有字符串从 .NET 转换成 UTF-8编码。 UTF-8 是一种可变长度字符编码,其中 1-4 个 8 位字节表示一个字符。 它后向兼容 ASCII,因此其编码比面向纯 ASCII 文本的 UTF-16 更紧凑。 也有 UTF-8 生成的字符串比 UTF-16 长的情况,但对本 tutorial password manager 来说,我们接受此类折衷情况。

商用应用会根据用户的本地语言选择最佳的编码方式,并将该编码方式记录在仓库中(以便使用其他本地语言在系统上打开仓库时,知道使用哪种编码进行创建)。

示例代码

如简介部分所述,本部分提供示例代码供您下载。 随附档案包含面向 Tutorial Password Manager 桥接 DLL 和安全区 DLL 的源代码。 此时安全区函数只是桩文件,我们将在第五部分对其进行填充。

即将推出

第五部分通过将 Crypto、DRNG 和 Vault 类移植到安全区内,并将其连接至 ECALL,从而完成安全区开发。 敬请关注!


英特尔® Software Guard Extensions 教程系列: 第五部分,安全区开发

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)教程系列第五部分将介绍如何开发面向 Tutorial Password Manager 应用的安全区。 在本系列的第四部分,我们创建了 DLL 用作安全区桥接函数与 C++/CLI 程序内核之间的接口层,并定义了安全区界面。 准备好这些组件后,现在我们可以将重点放在安全区上。

文章英特尔® Software Guard Extensions 教程系列简介列举了所有已经发布的教程。

本系列的安装部分将提供源代码:包含安全区的已完成应用。 该版本经过硬编码,可运行英特尔 SGX 代码路径。

安全区组件

为了确定需在安全区内实施的组件,我们参考第三部分图 1 中有关应用内核的类图。 与之前一样,将驻留在安全区内的对象涂成绿色,非信任组件涂成蓝色。


图 1.基于英特尔® Software Guard Extensions 的 Tutorial Password Manager 应用的类图。

通过该图我们可确定以下四个需要移植的类:

  • Vault
  • AccountRecord
  • Crypto
  • DRNG

不过开始之前,我们需要制定设计决策。 不论系统是否支持英特尔 SGX,应用都必须能够在该系统上运行,而且这意味着如果要使现有的类在安全区内运行,不能只是简单地进行转换。 我们必须创建两个版本:一个在安全区内使用,另一个在非信任内存中使用。 问题是,如何实施这种双重支持?

选项 1: 条件编译

第一种选择是在相同的源模块中实施安全区和非信任功能,并使用预处理器定义和 #ifdef语句根据上下文编译相应的代码。 这种方法的优点是,每个类只需一个源文件,因此不需要维护两个地方的变动。 而缺点是,代码读取难度较大,尤其是两个版本之间变动较大或较明显的情况下,而且项目结构会更加复杂。 两个 Visual Studio* 项目 — EnclavePasswordManagerCore将共享源文件,而且每个项目都需要设置预处理器符号,以确保编译相应的源代码。

选项 2: 独立类

第二种选择是复制需前往安全区的源文件。 这种方法的优点是,安全区有自己的源文件副本(我们不能直接修改),有助于简化项目结构和代码视图。 但同时伴随着一定的代价:如果需要对类进行修改,必须在两个地方进行,即使安全区和非信任版本的修改大体相同。

选项 3: 继承

第三种选择是使用类继承的 C++ 特性。 两个类版本所共有的功能在基类中实施,衍生的类将实施特定于分支的方法。 这种方法具有一个显著的优点:它使用专门设计的语言的特性,完全满足我们的需求,从而能够自然、有效地解决问题。 而缺点是增加了项目结构和代码本身的复杂性。

这里不存在任何严格、速效的准则,而且我们也不需要制定一劳永逸的决策。 实用的经验法则是:选项 1 适用于变动较少或能够简单划分的模块,选项 2 和 3 适用于变动较大或导致源代码难以读取和维护的情况。 不过最后都可归结为方式和偏好,每种方法都非常实用。

现在我们选择选项 2,因为该方法有助于轻松地并排对比安全区和非信任源文件。 在本教程后续的安装部分,我们将选择选项 3 来加强代码。

安全区类

在适应安全区方面,每个类都面临各自的问题和挑战,不过有一种普遍适用的方法:释放内存之前无需进行零填充。 大家回顾一下,第三部分推荐了一种在非信任内存中处理安全数据的方法。 由于 CPU 使用任何硬件层都不可用的加密密钥对安全区内存进行加密,因此释放的内存中包含对其他应用来说似乎是随机数据的内容。 这意味着我们可以删除安全区内所有针对 SecureZeroMemory 的调用。

Vault 类

Vault类是执行密码仓库操作的接口。 所有桥接函数均通过 Vault.中的一种或多种方法执行。 Vault.h的声明如下。

class PASSWORDMANAGERCORE_API Vault
{
	Crypto crypto;
	char m_pw_salt[8];
	char db_key_nonce[12];
	char db_key_tag[16];
	char db_key_enc[16];
	char db_key_obs[16];
	char db_key_xor[16];
	UINT16 db_version;
	UINT32 db_size; // Use get_db_size() to fetch this value so it gets updated as needed
	char db_data_nonce[12];
	char db_data_tag[16];
	char *db_data;
	UINT32 state;
	// Cache the number of defined accounts so that the GUI doesn't have to fetch
	// "empty" account info unnecessarily.
	UINT32 naccounts;

	AccountRecord accounts[MAX_ACCOUNTS];
	void clear();
	void clear_account_info();
	void update_db_size();

	void get_db_key(char key[16]);
	void set_db_key(const char key[16]);

public:
	Vault();
	~Vault();

	int initialize();
	int initialize(const unsigned char *header, UINT16 size);
	int load_vault(const unsigned char *edata);

	int get_header(unsigned char *header, UINT16 *size);
	int get_vault(unsigned char *edata, UINT32 *size);

	UINT32 get_db_size();

	void lock();
	int unlock(const char *password);

	int set_master_password(const char *password);
	int change_master_password(const char *oldpass, const char *newpass);

	int accounts_get_count(UINT32 *count);
	int accounts_get_info_sizes(UINT32 idx, UINT16 *mbname_sz, UINT16 *mblogin_sz, UINT16 *mburl_sz);
	int accounts_get_info(UINT32 idx, char *mbname, UINT16 mbname_sz, char *mblogin, UINT16 mblogin_sz,
		char *mburl, UINT16 mburl_sz);

	int accounts_get_password_size(UINT32 idx, UINT16 *mbpass_sz);
	int accounts_get_password(UINT32 idx, char *mbpass, UINT16 mbpass_sz);

	int accounts_set_info(UINT32 idx, const char *mbname, UINT16 mbname_len, const char *mblogin, UINT16 mblogin_len,
		const char *mburl, UINT16 mburl_len);
	int accounts_set_password(UINT32 idx, const char *mbpass, UINT16 mbpass_len);

	int accounts_generate_password(UINT16 length, UINT16 pwflags, char *cpass);

	int is_valid() { return _VST_IS_VALID(state); }
	int is_locked() { return ((state&_VST_LOCKED) == _VST_LOCKED) ? 1 : 0; }
};

安全区版的类声明(为清晰起见我们称之为 E_Vault)大体相同,除了有一个关键变动之外:数据库密钥处理。

在非信任代码路径中,Vault对象必须将加密的数据库密钥保存在内存中。 只要更改密码仓库,就必须加密最新的仓库数据,并将其写入磁盘,而且这意味着我们必须自行处理密钥。 有四种方法:

  1. 每次更改时向用户提示主密码,以便能够按照需求衍生数据库密钥。
  2. 缓存用户的主密码,以便在无需用户介入的情况下按照需求衍生数据库密钥。
  3. 加密、编码和/或模糊内存中的数据库密钥。
  4. 明文保存密钥。

这些方法都不太实用,而且必须使用英特尔 SGX 等技术。 第一种方法可能最安全,但用户不想运行以这种方式运行的应用。 通过 .NET* 中的 SecureString类能够有效使用第二种方法,但仍然容易受到调试程序的检查,而且密钥衍生功能会降低性能,这是用户所不能接受的。 第三种方法的安全性比第二种方法低,但不会影响性能。 第四种方法最糟糕。

我们的 Tutorial Password Manager 采用第三种方法:数据库密钥是一个包含 128 位值的 XOR’d,该数值是打开仓库文件时随机生成的,而且仅以这种 XOR’d 的形式保存在内存中。 这是一种有效的 运行调试程序的所有用户都可以查看该密钥,但它会限制数据库密钥以明文形式存在于内存中的时间。

void Vault::set_db_key(const char db_key[16])
{
	UINT i, j;
	for (i = 0; i < 4; ++i)
		for (j = 0; j < 4; ++j) db_key_obs[4 * i + j] = db_key[4 * i + j] ^ db_key_xor[4 * i + j];
}

void Vault::get_db_key(char db_key[16])
{
	UINT i, j;
	for (i = 0; i < 4; ++i)
		for (j = 0; j < 4; ++j) db_key[4 * i + j] = db_key_obs[4 * i + j] ^ db_key_xor[4 * i + j];
}

尽管比较模糊,但安全性很高,而且由于我们正在发布源代码,因此不是特别模糊。 我们可以选择更好的算法或增加长度来隐藏数据库密钥和平板电脑的机密密钥(包括它们在内存中的保存方式);最终我们选择的方法仍然容易受到调试程序的检查,而且任何人都能够看到该算法。

不过安全区内不存在这种问题。 经过硬件支持的加密,内存受到了很好的保护,即使数据库密钥已经解密,任何人也无法查看该密钥,即便以较高权限运行的进程也无法查看。 因此,我们不再需要这些类成员或方法:

char db_key_obs[16];
char db_key_xor[16];

	void get_db_key(char key[16]);
	void set_db_key(const char key[16]);

它们可由一个类成员取代:保留数据库密钥的 char阵列。

char db_key[16];

AccountInfo 类

帐号数据以 Vault对象成员的形式保存在大小固定的 AccountInfo对象阵列中。 Vault.h中还包含 AccountInfo声明,如下所示:

class PASSWORDMANAGERCORE_API AccountRecord
{
	char nonce[12];
	char tag[16];
	// Store these in their multibyte form. There's no sense in translating
	// them back to wchar_t since they have to be passed in and out as
	// char * anyway.
	char *name;
	char *login;
	char *url;
	char *epass;
	UINT16 epass_len; // Can't rely on NULL termination! It's an encrypted string.

	int set_field(char **field, const char *value, UINT16 len);
	void zero_free_field(char *field, UINT16 len);

public:
	AccountRecord();
	~AccountRecord();

	void set_nonce(const char *in) { memcpy(nonce, in, 12); }
	void set_tag(const char *in) { memcpy(tag, in, 16); }

	int set_enc_pass(const char *in, UINT16 len);
	int set_name(const char *in, UINT16 len) { return set_field(&name, in, len); }
	int set_login(const char *in, UINT16 len) { return set_field(&login, in, len); }
	int set_url(const char *in, UINT16 len) { return set_field(&url, in, len); }

	const char *get_epass() { return (epass == NULL)? "" : (const char *)epass; }
	const char *get_name() { return (name == NULL) ? "" : (const char *)name; }
	const char *get_login() { return (login == NULL) ? "" : (const char *)login; }
	const char *get_url() { return (url == NULL) ? "" : (const char *)url; }
	const char *get_nonce() { return (const char *)nonce; }
	const char *get_tag() { return (const char *)tag; }

	UINT16 get_name_len() { return (name == NULL) ? 0 : (UINT16)strlen(name); }
	UINT16 get_login_len() { return (login == NULL) ? 0 : (UINT16)strlen(login); }
	UINT16 get_url_len() { return (url == NULL) ? 0 : (UINT16)strlen(url); }
	UINT16 get_epass_len() { return (epass == NULL) ? 0 : epass_len; }

	void clear();
};

实际上我们不需要对该类进行任何处理,因为它在安全区内执行。 不用删除不必要的 SecureZeroFree调用,该类保持原样就可。 不过,我们需要对其进行修改,以说明一点:在安全区内,我们可以获得一定的灵活性,这在以前是无法实现的。

我们回到第三部分,该部分还介绍了另外一项有助于保护非信任内存空间中的数据的指南,即避免管理内存的容器类,尤其是标准模板库的 std::string类。 这一问题在安全区内也不存在。 原因一样,我们无需在释放内存之前对其进行零填充,也无需担心标准模板库 (STL) 容器如何管理内存。 安全区内存经过加密,即使因为容器操作而导致安全数据以碎片的形式存在,其他进程也无法查看这些数据。

在安全区内使用 std::string类的另外一个原因是:可靠性。 多年来,STL 容器后的代码一直受到同行的严格审查,因此,如果有选择的话,使用该代码比实施我们自己的高级字符串函数更加安全。 对于示例代码来说,比如 AccountInfo类中的代码,该问题可能不太明显,但如果是比较复杂的程序,这可能是一个巨大的优势。 不过它伴随的代价是,由于添加了 STL 代码,所以需要较大的 DLL。

新的类声明(我们称为 E_AccountInfo)如下所示:

#define TRY_ASSIGN(x) try{x.assign(in,len);} catch(...){return 0;} return 1

class E_AccountRecord
{
	char nonce[12];
	char tag[16];
	// Store these in their multibyte form. There's no sense in translating
	// them back to wchar_t since they have to be passed in and out as
	// char * anyway.
	string name, login, url, epass;

public:
	E_AccountRecord();
	~E_AccountRecord();

	void set_nonce(const char *in) { memcpy(nonce, in, 12); }
	void set_tag(const char *in) { memcpy(tag, in, 16); }

	int set_enc_pass(const char *in, uint16_t len) { TRY_ASSIGN(epass); }
	int set_name(const char *in, uint16_t len) { TRY_ASSIGN(name); }
	int set_login(const char *in, uint16_t len) { TRY_ASSIGN(login); }
	int set_url(const char *in, uint16_t len) { TRY_ASSIGN(url); }

	const char *get_epass() { return epass.c_str(); }
	const char *get_name() { return name.c_str(); }
	const char *get_login() { return login.c_str(); }
	const char *get_url() { return url.c_str(); }

	const char *get_nonce() { return (const char *)nonce; }
	const char *get_tag() { return (const char *)tag; }

	uint16_t get_name_len() { return (uint16_t) name.length(); }
	uint16_t get_login_len() { return (uint16_t) login.length(); }
	uint16_t get_url_len() { return (uint16_t) url.length(); }
	uint16_t get_epass_len() { return (uint16_t) epass.length(); }

	void clear();
};

tagnonce成员仍然以 char阵列的形式保存。 我们采用 GCM 模式,通过 AES 使用 128 位密钥、96 位随机数和 128 位认证标记完成密码加密。 由于随机数和标记的大小都是固定的,因此我们没有理由以简单 char阵列之外的其他形式保存它们。

请注意,这种基于 std::string的方法支持我们完成在头文件中定义类的几乎全部过程。

Crypto 类

Crypto类可为我们提供加密函数。 类声明如下所示。

class PASSWORDMANAGERCORE_API Crypto
{
	DRNG drng;

	crypto_status_t aes_init (BCRYPT_ALG_HANDLE *halgo, LPCWSTR algo_id, PBYTE chaining_mode, DWORD chaining_mode_len, BCRYPT_KEY_HANDLE *hkey, PBYTE key, ULONG key_len);
	void aes_close (BCRYPT_ALG_HANDLE *halgo, BCRYPT_KEY_HANDLE *hkey);

	crypto_status_t aes_128_gcm_encrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE pt, DWORD pt_len, PBYTE ct, DWORD ct_sz, PBYTE tag, DWORD tag_len);
	crypto_status_t aes_128_gcm_decrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE ct, DWORD ct_len, PBYTE pt, DWORD pt_sz, PBYTE tag, DWORD tag_len);
	crypto_status_t sha256_multi (PBYTE *messages, ULONG *lengths, BYTE hash[32]);

public:
	Crypto(void);
	~Crypto(void);

	crypto_status_t generate_database_key (BYTE key_out[16], GenerateDatabaseKeyCallback callback);
	crypto_status_t generate_salt (BYTE salt[8]);
	crypto_status_t generate_salt_ex (PBYTE salt, ULONG salt_len);
	crypto_status_t generate_nonce_gcm (BYTE nonce[12]);

	crypto_status_t unlock_vault(PBYTE passphrase, ULONG passphrase_len, BYTE salt[8], BYTE db_key_ct[16], BYTE db_key_iv[12], BYTE db_key_tag[16], BYTE db_key_pt[16]);

	crypto_status_t derive_master_key (PBYTE passphrase, ULONG passphrase_len, BYTE salt[8], BYTE mkey[16]);
	crypto_status_t derive_master_key_ex (PBYTE passphrase, ULONG passphrase_len, PBYTE salt, ULONG salt_len, ULONG iterations, BYTE mkey[16]);

	crypto_status_t validate_passphrase(PBYTE passphrase, ULONG passphrase_len, BYTE salt[8], BYTE db_key[16], BYTE db_iv[12], BYTE db_tag[16]);
	crypto_status_t validate_passphrase_ex(PBYTE passphrase, ULONG passphrase_len, PBYTE salt, ULONG salt_len, ULONG iterations, BYTE db_key[16], BYTE db_iv[12], BYTE db_tag[16]);

	crypto_status_t encrypt_database_key (BYTE master_key[16], BYTE db_key_pt[16], BYTE db_key_ct[16], BYTE iv[12], BYTE tag[16], DWORD flags= 0);
	crypto_status_t decrypt_database_key (BYTE master_key[16], BYTE db_key_ct[16], BYTE iv[12], BYTE tag[16], BYTE db_key_pt[16]);

	crypto_status_t encrypt_account_password (BYTE db_key[16], PBYTE password_pt, ULONG password_len, PBYTE password_ct, BYTE iv[12], BYTE tag[16], DWORD flags= 0);
	crypto_status_t decrypt_account_password (BYTE db_key[16], PBYTE password_ct, ULONG password_len, BYTE iv[12], BYTE tag[16], PBYTE password);

	crypto_status_t encrypt_database (BYTE db_key[16], PBYTE db_serialized, ULONG db_size, PBYTE db_ct, BYTE iv[12], BYTE tag[16], DWORD flags= 0);
	crypto_status_t decrypt_database (BYTE db_key[16], PBYTE db_ct, ULONG db_size, BYTE iv[12], BYTE tag[16], PBYTE db_serialized);

	crypto_status_t generate_password(PBYTE buffer, USHORT buffer_len, USHORT flags);
};

此类中的公开方法经过设计,能够执行多项高级仓库操作: unlock_vaultderive_master_keyvalidate_passphraseencrypt_database等。 这些方法完成任务时需调用一种或多种加密算法。 例如,unlock_vault方法提取用户提供的口令,通过基于 SHA-256 的密钥衍生功能运行该口令,并使用生成的密钥解密以 GCM 模式使用 AES-128 的数据库密钥。

不过,这些高级方法不直接调用加密基元, 而是调用中层,以独立函数的形式实施加密算法。


图 2.加密库依赖性。

构成中层的私有方法基于加密基元构建,并支持底层加密库提供的函数,如图 2 所示。 非英特尔 SGX 实施依赖于 Microsoft 的 Cryptography API: Next Generation (CNG) 来执行这些操作,但我们不能在安全区中使用同一种库,因为安全区不能依赖外部 DLL。 为构建此类的英特尔 SGX 版本,我们需要将底层函数替换为可信加密库(与英特尔 SGX SDK 一同发布)中的函数。 (大家可能还记得第二部分所述,我们谨慎选择与 CNG 和英特尔 SGX 可信加密库相同的加密函数,也正是出于这个原因。)

为创建支持安全区的 Crypto类(我们称之为 E_Crypto),我们需要修改这些私有方法:

crypto_status_t aes_128_gcm_encrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE pt, DWORD pt_len, PBYTE ct, DWORD ct_sz, PBYTE tag, DWORD tag_len);
	crypto_status_t aes_128_gcm_decrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE ct, DWORD ct_len, PBYTE pt, DWORD pt_sz, PBYTE tag, DWORD tag_len);
	crypto_status_t sha256_multi (PBYTE *messages, ULONG *lengths, BYTE hash[32]);

表 1 介绍了各种方法以及支持构建这些方法的 CNG 所提供的基元和支持函数。

方法

算法

CNG 基元和支持函数

aes_128_gcm_encrypt

采用 GCM 模式的 AES 加密包含:

  • 128 位密钥
  • 128 位认证标记
  • 无附加认证数据 (AAD)

BCryptOpenAlgorithmProvider
BCryptSetProperty
BCryptGenerateSymmetricKey
BCryptEncrypt
BCryptCloseAlgorithmProvider
BCryptDestroyKey

aes_128_gcm_decrypt

采用 GCM 模式的 AES 加密包含:

  • 128 位密钥
  • 128 位认证标记
  • 无 AAD

BCryptOpenAlgorithmProvider
BCryptSetProperty
BCryptGenerateSymmetricKey
BCryptDecrypt
BCryptCloseAlgorithmProvider
BCryptDestroyKey

sha256_multi

SHA-256 hash(增量)

BCryptOpenAlgorithmProvider
BCryptGetProperty
BCryptCreateHash
BCryptHashData
BCryptFinishHash
BCryptDestroyHash
BCryptCloseAlgorithmProvider

表 1.Crypto类方法映射至 Cryptography API: Next Generation 函数

CNG 支持精细地控制其加密算法,以及多种性能优化方法。 Crypto类的效率实际上非常低:每次调用这些算法时,都需要从头初始化底层基元,然后将其完全关闭。 对于密码管理器来说该问题不算严重,因为该管理器由 UI 驱动,而且每次仅加密一小部分数据。 高性能服务器应用(比如 Web 或数据库服务器)可能需要更为复杂的方法。

相比于 CNG 来说,可信加密库(与英特尔 SGX SDK 一同发布)的 API 更加类似于中层。 它对底层基元的控制不那么精细,但能够帮助我们简化 E_Crypto类的开发过程。 表 2 显示了中层与底层基元之间全新的映射过程。

方法

算法

英特尔® SGX 可信加密库基元和支持函数

aes_128_gcm_encrypt

采用 GCM 模式的 AES 加密包含:

  • 128 位密钥
  • 128 位认证标记
  • 无附加认证数据 (AAD)

sgx_rijndael128GCM_encrypt

aes_128_gcm_decrypt

采用 GCM 模式的 AES 加密包含:

  • 128 位密钥
  • 128 位认证标记
  • 无 AAD

sgx_rijndael128GCM_decrypt

sha256_multi

SHA-256 hash(增量)

sgx_sha256_init
sgx_sha256_update
sgx_sha256_get_hash
sgx_sha256_close

表 2.Crypto类方法映射至英特尔® SGX 可信加密库函数

DRNG 类

DRNG类是片上数字随机数生成器的接口,由英特尔® 安全密钥提供。 为了与之前的操作保持一致,我们将此类的安全区版本命名为 E_DRNG

我们对此类进行两项修改,以使其适用于安全区,但这两项修改都需要在类方法内部进行。 类声明将保持不变。

CPUID 指令

其中一项应用要求是 CPU 支持英特尔安全密钥。 即使英特尔 SGX 比安全密钥更新,但也无法保证未来所有支持英特尔 SGX 的 CPU 也同时支持英特尔安全密钥。 虽然现在我们难以想象这种情况,但最好不要假设特性之间存在某种根本不存在的联系。 如果特性有独立的检测机制,那么必须假设这些特性相互之间是独立的,并单独检查这些特性。 这意味着,支持英特尔 SGX 的 CPU 同样支持英特尔安全密钥这种假设无论具有多大的吸引力,在任何情况下我们都绝对不能这么做

令这种情况更为复杂的是,英特尔安全密钥实际上包含两种独立的特性,而且都必须单独检查。 应用必须确定是否同时支持 RDRAND 和 RDSEED 指令。 更多有关英特尔安全密钥的信息,请参阅英特尔® 数字随机数生成器 (DRNG) 软件实施指南

DRNG 类中的构造函数负责查看 RDRAND 和 RDSEED 特性检测。 它要求必须使用编译器内联函数 __cpuid__cpuidex调用 CPUID 指令,并通过结果设置静态的全局变量。

static int _drng_support= DRNG_SUPPORT_UNKNOWN;
static int _drng_support= DRNG_SUPPORT_UNKNOWN;

DRNG::DRNG(void)
{
	int info[4];

	if (_drng_support != DRNG_SUPPORT_UNKNOWN) return;

	_drng_support= DRNG_SUPPORT_NONE;

	// Check our feature support

	__cpuid(info, 0);

	if ( memcmp(&(info[1]), "Genu", 4) ||
		memcmp(&(info[3]), "ineI", 4) ||
		memcmp(&(info[2]), "ntel", 4) ) return;

	__cpuidex(info, 1, 0);

	if ( ((UINT) info[2]) & (1<<30) ) _drng_support|= DRNG_SUPPORT_RDRAND;

#ifdef COMPILER_HAS_RDSEED_SUPPORT
	__cpuidex(info, 7, 0);

	if ( ((UINT) info[1]) & (1<<18) ) _drng_support|= DRNG_SUPPORT_RDSEED;
#endif
}

E_DRNG类存在的问题是 CPUID 不是安全区内的合法指令。 为了调用 CPUID,必须使用 OCALL 退出安全区,然后调用非信任代码中的 CPUID。 幸运的是,英特尔 SGX SDK 设计者创建了两种便利的函数,可大大简化这一任务: sgx_cpuidsgx_cpuidex。 这些函数可自动执行 OCALL,而且 OCALL 也是自动生成的。 唯一的要求是 EDL 文件必须导入 sgx_tstdc.edl头文件:

enclave {

	/* Needed for the call to sgx_cpuidex */
	from "sgx_tstdc.edl" import *;

    trusted {
        /* define ECALLs here. */

		public int ve_initialize ();
		public int ve_initialize_from_header ([in, count=len] unsigned char *header, uint16_t len);
		/* Our other ECALLs have been omitted for brevity */
	};

    untrusted {
    };
};

E_DRNG构造函数中的特性检测代码将变成:

static int _drng_support= DRNG_SUPPORT_UNKNOWN;

E_DRNG::E_DRNG(void)
{
	int info[4];
	sgx_status_t status;

	if (_drng_support != DRNG_SUPPORT_UNKNOWN) return;

	_drng_support = DRNG_SUPPORT_NONE;

	// Check our feature support

	status= sgx_cpuid(info, 0);
	if (status != SGX_SUCCESS) return;

	if (memcmp(&(info[1]), "Genu", 4) ||
		memcmp(&(info[3]), "ineI", 4) ||
		memcmp(&(info[2]), "ntel", 4)) return;

	status= sgx_cpuidex(info, 1, 0);
	if (status != SGX_SUCCESS) return;

	if (info[2]) & (1 << 30)) _drng_support |= DRNG_SUPPORT_RDRAND;

#ifdef COMPILER_HAS_RDSEED_SUPPORT
	status= __cpuidex(info, 7, 0);
	if (status != SGX_SUCCESS) return;

	if (info[1]) & (1 << 18)) _drng_support |= DRNG_SUPPORT_RDSEED;
#endif
}

 

 

由于调用 CPUID 指令必须在非信任内存中进行,因此 CPUID 的结果不可信。 这一告警表示你可以运行 CPUID,也可以让 SGX 函数代劳。 英特尔 SGX SDK 建议: “代码应该验证结果并执行线程评估,以确定结果遭到欺骗后对可信代码所造成的影响。”

在我们的 tutorial password manager 中,可能会出现三种结果:

  1. 未检测到 RDRAND 和/或 RDSEED,但一种或两种函数的阳性结果遭到欺骗。 这将导致运行时出现非法指令故障,届时程序将会崩溃。
     
  2. 检测到 RDRAND,但阴性结果遭到欺骗。 这将导致运行时出现错误,使程序由于未检测到所需特性而体面地推出。
     
  3. 检测到 RDSEED,但阴性结果遭到欺骗。 这将导致程序返回至 seed-from-RDRAND 方法,以生成随机种子,此操作会稍微影响性能。 否则程序将正常运行。
最糟糕的场景是拒绝服务问题,但不会影响应用的机密或稳定性,因此我们不尝试检测欺骗攻击。

通过 RDRAND 生成种子

如果底层 CPU 不支持 RDSEED 指令,那么我们必须能够使用 RDRAND 指令生成随机种子,其功能与 RDSEED 提供的种子相同(如果可用)。 英特尔数字随机数生成器 (DRNG) 软件实施指南详细介绍了如何通过 RDRAND 获取随机种子,但还有一种更加简单的方法,即生成 512 对 128 位数值,并使用 CBC-MAC 模式的 AES 将这些中间值混合在一起,以生成一个 128 位的种子。 可根据需要重复该流程,以生成多个种子。

在非英特尔 SGX 代码路径中,seed_from_rdrand方法使用 CNG 构建加密算法。 由于英特尔 SGX 代码路径不依赖 CNG,因此我们需要再次使用与英特尔 SGX SDK 一同发布的可信加密库。 表 3 对更改进行了汇总。

算法

CNG 基元和支持函数

英特尔® SGX 可信加密库基元和支持函数

aes-cmac

BCryptOpenAlgorithmProvider
BCryptGenerateSymmetricKey
BCryptSetProperty
BCryptEncrypt
BCryptDestroyKey
BCryptCloseAlgorithmProvider

sgx_cmac128_init
sgx_cmac128_update
sgx_cmac128_final
sgx_cmac128_close

表 3.加密函数对 E_DRNG类的 seed_from_rdrand方法所做的更改

该算法为何嵌入至 DRNG类,而不与其加密算法一样在 Crypto类中实施? 这只是一种设计决策。 DRNG类只需这一种算法,因此我们决定不在 DRNGCrypto(目前Crypto确实依赖 DRNG)之间创建相互依赖性。 Crypto同样进行了结构设计,可为向仓库操作(而非作为通用加密 API 的函数)提供加密服务。

为何不使用 sgx_read_rand?

英特尔提供函数 sgx_read_rand,作为在安全区内获取随机数的方法。 我们不使用该函数的原因有以下三点:

  1. 如英特尔 SGX SDK 文档中所述,该函数“用于替代安全区内的 C 标准伪随机序列生成函数,因为安全区内不支持这些标准函数,比如 randsrand等”。尽管在 CPU 支持的情况下 sgx_read_rand确实调用 RDRAND 指令,但如果不支持,它会返回至可信 C 库的 srandrand实施。 C 库生成的随机数不适用于加密。 基本不可能出现这种情况,但我们在 CPUID 部分说过,绝不能假设这种情况永远不会出现。
  2. 这里没有用于调用 RDSEED 指令的英特尔 SGX SDK 函数,因此仍然需要在代码中使用编译器内联函数。 尽管能够用调用 sgx_read_rand替换 RDRAND 内联函数,但对代码管理或结构没有丝毫帮助,而且还会导致操作时间延长。
  3. 该内联函数的性能比 sgx_read_rand稍微高一些,因为最终代码中少一个函数调用层。

总结

通过更改代码,我们获得了一个功能全面的安全区。 但它的实施效率并不高,而且功能方面存在差距。我们将在第七部分和第八部分重新探讨安全区设计,使这些问题得以解决。

如简介部分所述,本部分提供示例代码供您下载。 随附档案包含面向 Tutorial Password Manager 内核的源代码,包括安全区和包装程序函数。

即将推出

本教程的第六部分将为密码管理器添加动态特性检测机制,以支持其根据底层平台是否支持英特尔 SGX 等情况选择合适的代码路径。 敬请关注!

英特尔® Software Guard Extensions 教程系列: 第六部分,双代码路径

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)) 教程系列的第六部分,我们先将安全区搁在一旁,来处理第二部分应用设计中提出的重要设计要求:支持双代码路径。 我们希望无论主机是否具备英特尔 SGX 功能,都能够支持 Tutorial Password Manager 运行。 本部分的大多数内容摘录自文章准确检测应用中的英特尔® Software Guard 扩展

文章英特尔® Software Guard Extensions 教程系列简介列举了所有已经发布的教程。

本系列的安装部分将提供源代码。

所有英特尔® Software Guard Extensions 应用都需要双代码路径

首先必须指出,所有英特尔 SGX 应用都需要双代码路径。 即使应用编写后只能在英特尔 SGX 可用并已启用的情况下执行,也必须有备用代码路径以向用户显示实用的错误信息,然后体面地退出。

简言之,应用不能仅仅因为平台不支持英特尔 SGX 而出现崩溃或无法启动的情况。

确定问题

第五部分我们完成了第一版应用安全区,并通过硬编码安全区支持,对其进行了测试。 具体做法是在 PasswordCoreNative.cpp 中设置 _supports_sgx标记。

PasswordManagerCoreNative::PasswordManagerCoreNative(void)
{
	_supports_sgx= 1;
	adsize= 0;
	accountdata= NULL;
	timer = NULL;
}

显然,我们不能将其默认为打开状态。 特性检测的惯例是特性默认处于关闭状态,如果需要进行检测,再将其打开。 因此第一步是取消更改,重新将标记设置为 0,以有效禁用英特尔 SGX 代码路径。

PasswordManagerCoreNative::PasswordManagerCoreNative(void)
{
	_supports_sgx= 0;
	adsize= 0;
	accountdata= NULL;
	timer = NULL;
}

不过,进入特性检测流程之前,我们对运行测试套件(CLI 测试应用)的控制台应用进行一次快速的功能测试,具体方法是在不支持英特尔 SGX 特性的老旧系统上执行该应用。 标记设置为 0 时,应用将不选择英特尔 SGX 代码路径,并且应该能够正常运行。

以下是基于第四代智能英特尔® 酷睿™ i7 处理器,运行 64 位 Microsoft Windows* 8.1 的笔记本电脑的输出。 该系统不支持英特尔 SGX。

CLI 测试应用

结果如何?

显然,即使软件中明确禁用了英特尔 SGX 代码路径,我们也遇到了问题。 该应用经过编写,无法在不支持英特尔 SGX 的系统上执行。 它甚至没有开始执行。 具体情况如何?

控制台窗口中显示的错误消息给出了提示:

System.IO.FileNotFoundException: 无法加载文件或汇编‘PasswordManagerCore.dll’或其依赖性。 找不到特定文件。

我们首先查看 PasswordManagerCore.dll 及其依赖性:

其他依赖性

除了核心操作系统库外,我们还依赖 bcrypt.lib and EnclaveBridge.lib,它在运行时需要 bcrypt.dllEnclaveBridge.dll。 由于 bcrypt.dll来源于 Microsoft 并包含在操作系统中,因此我们有理由假定依赖性已安装(如有), 从而留下了 EnclaveBridge.dll

检查依赖性,我们看到:

其他依赖性

这就是问题所在。 即使明确禁用了英特尔 SGX 代码路径,EnclaveBridge.dll依然能够参考英特尔 SGX 运行时库。 加载后,必须解析对象模块中的所有符号。 是否禁用英特尔 SGX 代码路径都无关紧要:未定义符号仍然在 DLL 中。 PasswordManagerCore.dll加载时,通过加载 bcrypt.dllEnclaveBridge.dll解析未定义符号,后者反过来通过加载 sgx_urts.dllsgx_uae_service.dll尝试解析未定义符号。 支持我们尝试运行命令行测试应用的系统没有自己的库,而且由于操作系统无法解析所有符号,因此会出现异常,甚至程序在启动之前崩溃的情况。

这两个 DLL 是英特尔 SGX 平台软件 (PSW) 包的一部分,没有它们,用英特尔 SGX 软件开发套件 (SDK) 编写的英特尔 SGX 应用将无法执行。 即使没有这些库,我们的应用也必须能够运行。

平台软件包

如之前所述,运行时库是 PSW 的一部分。 除了这些支持库外,PSW 还包括:

  • 在系统上支持并维护可信计算模块 (TCB) 的服务
  • 执行和管理部分英特尔 SGX 操作(比如验证)的服务
  • 连接可信时间和单调计数器等平台服务的接口

部署英特尔 SGX 应用时,必须由应用安装程序来安装 PSW,因为英特尔不支持最终用户直接下载 PSW。 软件厂商不能假定目标系统安装了该 PSW。 事实上,英特尔 SGX 许可协议明确规定,被许可方必须根据应用重新分发 PSW。

本系列的安装部分将详细介绍 PSW 安装程序,包括如何打包和部署。

检测英特尔 Software Guard Extensions 支持

到目前为止我们一直专注于在不支持英特尔 SGX — 具体来说是没有 PSW — 的情况下启动系统中的应用。 接下来检测应用运行过程中是否存在并启用了英特尔 SGX 支持。

遗憾的是,英特尔 SGX 特性检测流程非常复杂。 想要系统支持英特尔 SGX,必须满足以下四个条件:

  1. CPU 必须支持英特尔 SGX。
  2. BIOS 必须支持英特尔 SGX。
  3. 在 BIOS 中必须明确启用英特尔 SGX 并将其设置为“software controlled”状态。
  4. 平台必须安装 PSW。

请注意,仅 CPUID 指令并不足以检测平台是否支持英特尔 SGX。 它可告知 CPU 是否支持该特性,但无法提供有关 BIOS 配置或系统是否安装了该软件的信息。 仅依靠 CPUID 结果来制定有关英特尔 SGX 支持的决策可能会导致出现运行时故障。

特性检测更加困难,检查 BIOS 状态并非易事,而且用户通常无法完成这项操作。 幸运的是,英特尔 SGX SDK 提供了一种简单的方法:函数 sgx_enable_device 既能检查系统是否支持英特尔 SGX,还能在 BIOS 设置为软件控制状态时尝试启用该软件(软件控制设置的目的在于支持应用启用英特尔 SGX,用户无需重启系统或进入 BIOS 设置屏幕(对于非技术用户来说是一项异常艰巨的任务))。

但 sgx_enable_device 存在的问题是,它是英特尔 SGX 运行时的一部分,这意味着系统必须安装 PSW 才能使用该函数。 因此在尝试调用 sgx_enable_device 之前,必须首先检测 PSW 是否存在。

实施

确定问题后,现在我们安排必须遵循的步骤,以便双代码路径正常运行。 应用必须:

  1. 即使没有英特尔 SGX 运行时库也依然能够加载并开始执行。
  2. 确定是否安装了 PSW 软件包。
  3. 确定是否启用了英特尔 SGX(并尝试启用)。

在没有英特尔 Software Guard Extensions 运行时的情况下加载和执行

我们的主应用依赖 PasswordManagerCore.dll,后者依赖 EnclaveBridge.dll,而它又反过来依赖英特尔 SGX 运行时。 由于应用加载时需要解析所有符号,因此我们需要防止加载程序解析来自于英特尔 SGX 运行时库的符号。 有以下两种选择:

选择一: 动态加载      

在动态加载过程中,不要明确链接项目中的库。 而是通过系统调用在运行时加载库,然后查看计划使用的函数名称,以便获取它们在内存中的地址。 之后通过函数指示器间接调用库中的函数。

动态加载非常麻烦。 即使只需几个函数,也必须不厌其烦地为所需的函数构建函数指示器原型,并获取加载地址(一次一个)。 由于无法按照名称明确调用函数,因此会失去集成开发环境的部分优势(比如原型辅助)。

动态加载通常用于可扩展应用架构(例如插件)。

选择二: 延迟加载 DLL

采用此方法时,动态链接项目中的所有库,但需要通知 Windows 延迟加载问题 DLL。 延迟加载 DLL 时,Windows 不会在应用启动时尝试解析 DLL 定义的符号, 而是等待程序首次调用该 DLL 中定义的函数,DLL 在该点进行加载,符号(及依赖性)也完成解析。 这意味着只有在应用需要时才会加载 DLL。 此方法的优点是它支持应用引用未安装的 DLL,只要该 DLL 中的函数未经调用。

英特尔 SGX 特性标记关闭时,正好符合这种情况,因此我们选择第二种方法。

在面向从属应用或 DLL 的项目配置中指定需要延迟加载的 DLL。 就本 Tutorial Password Manager 而言,标记为延迟加载的最佳 DLL 为 EnclaveBridge.dll,因为如果启用英特尔 SGX 路径,我们只需调用该 DLL。 如果该 DLL 不加载,两个英特尔 SGX 运行时 DLL 也不加载。

我们设置 PasswordManagerCore.dll 项目配置 Linker -> Input页面中的选项:

密码管理器

第四代智能英特尔酷睿处理器系统重新构建并安装 DLL 后,控制台测试应用将按预期运行。

CLI 测试应用

检测平台软件包

调用 sgx_enable_device 函数检查平台是否支持英特尔 SGX 之前,首先需要确保 PSW 已安装,因为 sgx_enable_device 是英特尔 SGX 运行时的一部分。 最好的方法的尝试加载运行时库。

通过之前的步骤我们知道,不能直接动态加载,因为如果尝试在不支持英特尔 SGX(或没有安装 PSW 包)的系统上运行程序,会导致出现异常。 但我们也不能依赖延迟加载 DLL:延迟加载无法告诉我们库是否已安装,因为如果没有安装,应用将会崩溃! 这意味着我们必须使用动态加载测试是否存在运行时库。

PSW 运行时库应该安装在 Windows 系统目录中,以便我们使用 GetSystemDirectory 获取该路径,并通过调用 SetDllDirectory 限制 DLL 搜索路径。 最后使用 LoadLibrary 加载这两个库。 如果其中一种调用失败,我们将知道 PSW 尚未安装,而且主应用不会尝试运行英特尔 SGX 代码路径。

检测和启用英特尔 Software Guard Extensions

由于上一步骤动态加载了 PSW 运行时库,因此我们只能手动查看有关 sgx_enable_device 的符号,然后通过函数指示器调用该函数。 结果将告诉我们英特尔 SGX 是否已启用。

实施

我们将创建一个名为 FeatureSupport.dll 的新 DLL,以在 Tutorial Password Manager 中实施上述方法。 我们能够安全地通过主应用动态链接此 DLL,因为它不依赖其他 DLL。

特性检测将进入一个名为 FeatureSupport 的 C++/CLI 类,这样还会包含部分高级函数以获取有关英特尔 SGX 状态的信息。 在极少数情况下,通过软件启用英特尔 SGX 可能要求重启系统,此外,还会出现软件启用操作失败并迫使用户在 BIOS 中明确启用该特性的罕见情况。

针对 FeatureSupport 的类声明如下所示。

typedef sgx_status_t(SGXAPI *fp_sgx_enable_device_t)(sgx_device_status_t *);


public ref class FeatureSupport {
private:
	UINT sgx_support;
	HINSTANCE h_urts, h_service;

	// Function pointers

	fp_sgx_enable_device_t fp_sgx_enable_device;

	int is_psw_installed(void);
	void check_sgx_support(void);
	void load_functions(void);

public:
	FeatureSupport();
	~FeatureSupport();

	UINT get_sgx_support(void);
	int is_enabled(void);
	int is_supported(void);
	int reboot_required(void);
	int bios_enable_required(void);

	// Wrappers around SGX functions

	sgx_status_t enable_device(sgx_device_status_t *device_status);

};

以下低级例程可用于检查 PSW 包并尝试检测和启用英特尔 SGX。

int FeatureSupport::is_psw_installed()
{
	_TCHAR *systemdir;
	UINT rv, sz;

	// Get the system directory path. Start by finding out how much space we need
	// to hold it.

	sz = GetSystemDirectory(NULL, 0);
	if (sz == 0) return 0;

	systemdir = new _TCHAR[sz + 1];
	rv = GetSystemDirectory(systemdir, sz);
	if (rv == 0 || rv > sz) return 0;

	// Set our DLL search path to just the System directory so we don't accidentally
	// load the DLLs from an untrusted path.

	if (SetDllDirectory(systemdir) == 0) {
		delete systemdir;
		return 0;
	}

	delete systemdir; // No longer need this

	// Need to be able to load both of these DLLs from the System directory.

	if ((h_service = LoadLibrary(_T("sgx_uae_service.dll"))) == NULL) {
		return 0;
	}

	if ((h_urts = LoadLibrary(_T("sgx_urts.dll"))) == NULL) {
		FreeLibrary(h_service);
		h_service = NULL;
		return 0;
	}

	load_functions();

	return 1;
}

void FeatureSupport::check_sgx_support()
{
	sgx_device_status_t sgx_device_status;

	if (sgx_support != SGX_SUPPORT_UNKNOWN) return;

	sgx_support = SGX_SUPPORT_NO;

	// Check for the PSW

	if (!is_psw_installed()) return;

	sgx_support = SGX_SUPPORT_YES;

	// Try to enable SGX

	if (this->enable_device(&sgx_device_status) != SGX_SUCCESS) return;

	// If SGX isn't enabled yet, perform the software opt-in/enable.

	if (sgx_device_status != SGX_ENABLED) {
		switch (sgx_device_status) {
		case SGX_DISABLED_REBOOT_REQUIRED:
			// A reboot is required.
			sgx_support |= SGX_SUPPORT_REBOOT_REQUIRED;
			break;
		case SGX_DISABLED_LEGACY_OS:
			// BIOS enabling is required
			sgx_support |= SGX_SUPPORT_ENABLE_REQUIRED;
			break;
		}

		return;
	}

	sgx_support |= SGX_SUPPORT_ENABLED;
}

void FeatureSupport::load_functions()
{
	fp_sgx_enable_device = (fp_sgx_enable_device_t)GetProcAddress(h_service, "sgx_enable_device");
}

// Wrappers around SDK functions so the user doesn't have to mess with dynamic loading by hand.

sgx_status_t FeatureSupport::enable_device(sgx_device_status_t *device_status)
{
	check_sgx_support();

	if (fp_sgx_enable_device == NULL) {
		return SGX_ERROR_UNEXPECTED;
	}

	return fp_sgx_enable_device(device_status);
}

总结

通过修改代码,我们已将英特尔 SGX 特性检测集成至应用之中! 不管系统支不支持英特尔 SGX,它都将顺畅执行,并选择合适的代码分支。

如简介部分所述,本部分将提供示例代码供您下载。 随附档案包含面向 Tutorial Password Manager 内核的源代码,包括新的特性检测 DLL。 另外,我们添加了一个新的基于 GUI 的测试程序,可自动选择英特尔 SGX 代码路径,不过也支持您根据需要禁用该特性(此选项仅适用于系统支持英特尔 SGX 的情况)。

SGX 代码分支

基于控制台的测试程序已经过更新,可检测英特尔 SGX,不过它无法进行配置以在不修改源代码的情况下关闭英特尔 SGX。

即将推出

第七部分将重新介绍安全区,以对其界面进行微调。 敬请关注!

面向增强现实的自主导航介绍

$
0
0

下载 PDF
下载代码示例

Ryan Measel 和 Ashwin Sinha

1. 简介

感知计算代表着人机交互的最新发展。 它囊括了能够感知并理解物理环境的各项技术,包括手势、语音识别、面部识别、运动追踪和环境重构。 高级英特尔® 实感™ 摄像头 F200 和 R200 处于感知计算领域的最前沿。 深度感知功能支持 F200 和 R200 重构 3D 环境,并跟踪设备相对于环境的运动。 环境重构结合运动跟踪,可支持虚拟资产与现实环境实现相互融合,从而提供出色的增强现实体验。 

尽管英特尔实感摄像头可提供数据支持增强现实应用,但沉浸式体验的创建依然取决于开发人员。 使用自主代理是创造真实环境的方法之一。 自主代理指使用人工智能独立行动的实体。 人工智能定义操作参数以及代理必须遵守的规则。 代理以实时的方式动态响应环境,因此即使是简单设计也可创建复杂行为。

自主代理的存在方式有多种;但此处为了便于介绍,我们仅介绍用于移动和导航的代理。 这类代理的例子非常丰富,包括视频游戏中的非玩家角色 (NPC) 和教育动画中的鸟群。 代理的目的因应用的不同而有所差异,但其移动和导航原则大体相同。

本文旨在介绍自主导航,以及如何将其应用于增强现实应用。 我们开发一个采用英特尔实感摄像头 R200 和 Unity* 3D 游戏引擎的示例。 最好对英特尔® 实感™ SDK和 Unity 有一定的了解。 关于集成英特尔实感 SDK 和 Unity 的信息,请参阅: “Unity* 和英特尔® 实感™ 3D 摄像头助力游戏开发”和“第一印象: 英特尔® 实感™ R200 助力 Unity 中的增强现实”。

2. 自主导航

从实施和计算的角度来说,处理基于代理的导航的方法多种多样,从简单到复杂,各种方法一应俱全。 简单方法即为代理定义路径。 选择一个坐标点,代理按照该坐标点直线移动。 尽管实施起来非常简单,但该方法存在许多问题。 最明显的是:如果代理和坐标点之间不存在直线路径该怎么办(图 1)?

An agent moves along a straight path towards the target

图 1. 代理沿着直线路径朝目标移动,但该路径可能被障碍物阻挡。 注: 本次介绍的内容适用于 2D 和 3D 空间导航,但 2D 仅用于说明。

我们需要增加其他坐标点以绕过该障碍物(图 2)。

Additional waypoints are added to allow the agent to navigate around obstacles

图 2. 增加其他坐标点使代理绕过障碍物。

在障碍物多的大型地图中,通常会有更多的坐标点和路径。 另外,坐标点越密集(图 3),越能提高路径的效率(缩短到达目的地的行程)。

the number of waypoints and possible paths increases

图 3. 随着地图的扩大,坐标点和路径的数量也会大幅增加。

数量庞大的坐标点要求必须能够在非相邻坐标点之间找到一条路径。 这一问题被称为路径查找。 路径查找与图论息息相关。除了导航之外,路径查找还可广泛应用于多个领域。 因此,人们对该主题的研究也非常深入,开发了许多算法以解决相关问题。 A*是其中最为突出的一种路径查找算法。 简言之,该算法穿梭于前往目的地的相邻坐标点,并将所访问的坐标点及其与之相连的坐标点绘制成地图。 到达目的地后,该算法使用它生成的地图计算路径。 之后代理就可按照该路径移动。 A* 算法不搜索整个空间,因此无法确保最佳路径, 尽管在计算方面非常高效。

The A* algorithm traverses a map searching for a route to the target

图 4. A* 算法穿梭于地图之中搜索目标路线。动画,Subh83 / CC BY 3.0制作

A* 算法无法适应环境中的动态变化,比如添加/移除障碍物、移动边界等等。 从本质上来说,增强现实环境具有动态性,因为它们的创建和更改以用户的移动和物理空间为基础。

对动态环境来说,最好让代理实时制定决策,以便将对环境的最新了解融入到决策之中。 因此必须制定行为框架,支持代理实时制定决策并采取行动。 就导航而言,将行为框架分成三层不仅非常方便,而且非常普遍:

  1. 行动选择,包括设定目标和决定如何实现目标。 例如,一只小兔子正在四处寻找食物,看到附近出现了捕食者,决定立即逃走。 状态机可用于再现这种行为,因为该设备可定义代理的状态以及状态变化的前提条件。
  2. 操控指根据代理目前的状态计算移动。 如果捕食者追赶小兔子,小兔子应想方设法摆脱捕食者。 操控用于计算这种运动力的强度和方向。
  3. 移动指代理移动时所遵循的机制。 兔子、人类、汽车和宇宙飞船,其移动方式各不相同。 移动规定代理的移动方式(比如用腿、车轮、推进器等)以及运动参数(比如质量、最大速度、最大力等)。

这些层级共同组成了代理的人工智能。 第 3 节将展示一个 Unity 示例,以介绍这些层级的实施方法。 第 4 节介绍如何将自主导航集成至使用 R200 的增强现实应用。

3. 实施自主导航

本节将从移动开始,详细介绍 Unity 场景中面向自主导航的行为框架。

移动

代理的移动以牛顿运动定律(力是物体产生加速度的原因)为基础。 我们将使用质量分布均匀的简单模型,它能够使力作用于物体的各个方向。 为了限制移动,必须定义最大力和最大速度(表 1)。

public float mass = 1f;            // Mass (kg)
public float maxSpeed = 0.5f;      // Maximum speed (m/s)
public float maxForce = 1f;        // "Maximum force (N)

表 1. 代理移动模型。

该代理必须包含在启动阶段进行了初始化的刚体组件和对撞机组件(表 2)。 为简化模型,消除了刚体中的重力,但它是可以融入的。

private void Start () {

	// Initialize the rigidbody
	this.rb = GetComponent<rigidbody> ();
	this.rb.mass = this.mass;
	this.rb.useGravity = false;

	// Initialize the collider
	this.col = GetComponent<collider> ();
}

表 2. 在 Start() 上初始化刚体和对撞机。

在 FixedUpdate() 步骤中,力作用于刚体,可使代理移动(表 3)。 FixedUpdate() 与 Update() 类似,但它能够确保始终以相同的间隔执行(而 Update() 无法做到这点)。 FixedUpdate() 步骤完成后,Unity 引擎开始执行物理计算(有关刚体的运算)。

private void FixedUpdate () {

	Vector3 force = Vector3.forward;

	// Upper bound on force
	if (force.magnitude > this.maxForce) {
		force = force.normalized * this.maxForce;
	}

	// Apply the force
	rb.AddForce (force, ForceMode.Force);

	// Upper bound on speed
	if (rb.velocity.magnitude > this.maxSpeed) {
		rb.velocity = rb.velocity.normalized * this.maxSpeed;
	}
}

表 3. FixedUpdate() 步骤中力作用于刚体。 该示例沿着 Z 轴的方向移动代理。

如果力的大小超过了代理的最大受力,将调整至与最大受力相同(方向不变)。 AddForce () 函数采用数值积分法施加力:

方程 1. 速度数值积分。 AddForce() 函数执行该计算。

其中 表示新速度,表示之前的速度,表示力,表示质量,表示更新之间的时步(Unity 默认固定时步为 0.02 秒)。 如果速度超出代理的最大速度,将调整至与最大速度相同。

操控

操控计算提供给移动模型的力。 将实施三种操控行为:靠近、抵达和避障。

靠近

“靠近”行为尝试以最快的速度向目标前进。 该行为的预期速度以最大速度靠近目标。 操控力即代理的预期速度与当前速度之间的差额(图 5)。

The Seek behavior applies a steering force

图 5. “靠近”行为将操控力从当前速度作用于预期速度。

该实施(表 4)首先计算预期矢量,计算方法为归一代理与目标之间的偏差,然后乘以最大速度。 返回的操控力为预期速度减去当前速度,即刚体的速度。

private Vector3 Seek () {

	Vector3 desiredVelocity = (this.seekTarget.position - this.transform.position).normalized * this.maxSpeed;
	return desiredVelocity - this.rb.velocity;
}

表 4. “靠近”操控行为。

在 FixedUpdate() 步骤中计算力时,代理通过调用 Seek() 使用“靠近”行为(表 5)。

private void FixedUpdate () {

	Vector3 force = Seek ();
	...

表 5. 在 FixedUpdate () 中调用 Seek()。

视频 1 所示为作用中的“靠近”行为示例。 代理的蓝色箭头表示刚体的当前速度,红色箭头应用于该时步中的操控力。

视频 1. 由于代理的最初速度与目标方向彼此正交,所以其移动轨迹为曲线。

原视频链接地址:ttps://www.youtube.com/watch?v=HMjut52zxgI

抵达

“靠近”行为超越并围绕目标波动,因为它正以尽可能最快的速度抵达目标。 “抵达”行为与“靠近”行为类似,唯一的不同点是它会尝试在目标点完全停下来。 “减速半径”参数定义代理离目标多远时开始减速。 如果代理位于减速半径之中,预期速度调整为与代理与目标之间的距离成反比的速度。 代理可能无法完全停止,这取决于最大力、最大速度和减速半径。

“抵达”行为(表 6)首先计算代理与目标之间的距离。 调整速度即根据距离除以减速半径后的数据所调整的最大速度。 预期速度将视作调整速度与最大速度的最小值。 因此,如果与目标的距离小于减速半径,预期速度将为调整速度。 否则,预期速度将为最大速度。 该函数的其余部分的执行方式与使用预期速度的“靠近”行为几乎相同。

// Arrive deceleration radius (m)
public float decelerationRadius = 1f;

private Vector3 Arrive () {

	// Calculate the desired speed
	Vector3 targetOffset = this.seekTarget.position - this.transform.position;
	float distance = targetOffset.magnitude;
	float scaledSpeed = (distance / this.decelerationRadius) * this.maxSpeed;
	float desiredSpeed = Mathf.Min (scaledSpeed, this.maxSpeed);

	// Compute the steering force
	Vector3 desiredVelocity = targetOffset.normalized * desiredSpeed;
	return desiredVelocity - this.rb.velocity;
}

表 6. “抵达”操控行为。

视频 2. 代理达到目标时抵达行为开始减速。

原视频链接地址:https://www.youtube.com/watch?v=LsUDMG8KLn8

避障

“抵达”行为和“靠近”行为适合到达目的地,但不适合处理障碍。 在动态环境中,代理需要能够随时躲避出现的障碍物。 在预期路径中,“避障”行为比代理看得更远,并确定前方是否有需要躲避的障碍物。 如果发现障碍物,该行为计算将改变代理前进路径以躲避障碍物的力(图 6)。

图 6. 如果在当前前进轨迹中发现障碍物,将返回能够防止碰撞的力。

避障实施(表 7)通过球体投射来检测碰撞。 球体投射沿着刚体的当前速度矢量投射一个球体,并返回有关碰撞的 RaycastHit。 该球体从代理中心出发,其半径等于代理的对撞机半径加上“躲避半径”参数。 躲避半径支持用户定义代理周围的空隙。 该球体只能按照“前方检测”参数规定的距离前进。

// Avoidance radius (m). The desired amount of space between the agent and obstacles.
public float avoidanceRadius = 0.03f;
// Forward detection radius (m). The distance in front of the agent that is checked for obstacles.
public float forwardDetection = 0.5f;

private Vector3 ObstacleAvoidance () {

	Vector3 steeringForce = Vector3.zero;

	// Cast a sphere, that bounds the avoidance zone of the agent, to detect obstacles
	RaycastHit[] hits = Physics.SphereCastAll(this.transform.position, this.col.bounds.extents.x + this.avoidanceRadius, this.rb.velocity, this.forwardDetection);

	// Compute and sum the forces across all hits
	for(int i = 0; i < hits.Length; i++)    {

		// Ensure that the collidier is on a different object
		if (hits[i].collider.gameObject.GetInstanceID () != this.gameObject.GetInstanceID ()) {

			if (hits[i].distance > 0) {

				// Scale the force inversely proportional to the distance to the target
				float scaledForce = ((this.forwardDetection - hits[i].distance) / this.forwardDetection) * this.maxForce;
				float desiredForce = Mathf.Min (scaledForce, this.maxForce);

				// Compute the steering force
				steeringForce += hits[i].normal * desiredForce;
			}
		}
	}

	return steeringForce;
}

表 7. “避障”操控行为。

球体投射返回 RaycastHit 对象阵列。 RaycastHit 包含有关碰撞的信息,包括碰撞距离和碰撞表面的法线。 法线是与表面正交的矢量, 可用于使代理偏离碰撞点。 通过将最大力调整为与碰撞距离成反比的数值来确定力的大小。 每次碰撞的力相加,所得结果为单个时步的总操控力。

不同行为可进行组合,形成更复杂的行为(表 8)。 避障仅在与其他行为配合使用时才能发挥作用。 在本示例(视频 3)中,“避障”行为与“抵达”行为配合使用。 只需将它们各自的力相加,即可配合实施这两种行为。 我们还可创建更加复杂的方案,通过融入启发法,确定力的优先权系数。

private void FixedUpdate () {

	// Calculate the total steering force by summing the active steering behaviors
	Vector3 force = Arrive () + ObstacleAvoidance();
	...

表 8. 将“抵达”行为和“避障”行为的力相加,从而配合实施这两种行为。

视频 3. 代理配合使用“抵达”行为和“避障”行为。

原视频链接地址:https://www.youtube.com/watch?v=p_3-zU5GBEI

行动选择

行动选择指代理设定最高目标和制定决策。 通过组合“抵达”和“避障”行为,代理实施可融入简单的行动选择模型。 代理尝试着到达目的地,如果检测到障碍物,将调整其前进轨迹。 “避障”行为的“躲避半径”和“前方检测”参数会定义何时采取行动。

4. 集成 R200

由于代理能够自主导航,因此能够与增强现实应用相集成。

以下示例在附带英特尔实感 SDK 的“场景感知”示例的顶部构建。 该应用将使用“场景感知”创建一个网格,以便用户能够在该网格上设定和移动目标。 之后代理将围绕生成的网格进行导航,以到达目的地。

场景管理器

场景管理器脚本初始化场景并处理用户输入。 Touch up(或鼠标点击版,如果设备不支持触控功能)是唯一的输入。 触控点的光线投射将决定是否在生成的网格上进行触控。 首次触控在网格上生成目标;第二次触控生成代理;后续每次触控将移动目标的位置。 状态机处理控制逻辑(表 9)。

// State machine that controls the scene:
//         Start => SceneInitialized -> TargetInitialized -> AgentInitialized
private enum SceneState {SceneInitialized, TargetInitialized, AgentInitialized};
private SceneState state = SceneState.SceneInitialized;    // Initial scene state.

private void Update () {

	// Trigger when the user "clicks" with either the mouse or a touch up gesture.
	if(Input.GetMouseButtonUp (0)) {
		TouchHandler ();
	}
}

private void TouchHandler () {

	RaycastHit hit;

	// Raycast from the point touched on the screen
	if (Physics.Raycast (Camera.main.ScreenPointToRay (Input.mousePosition), out hit)) {

	 // Only register if the touch was on the generated mesh
		if (hit.collider.gameObject.name == "meshPrefab(Clone)") {

			switch (this.state) {
			case SceneState.SceneInitialized:
				SpawnTarget (hit);
				this.state = SceneState.TargetInitialized;
				break;
			case SceneState.TargetInitialized:
				SpawnAgent (hit);
				this.state = SceneState.AgentInitialized;
				break;
			case SceneState.AgentInitialized:
				MoveTarget (hit);
				break;
			default:
				Debug.LogError("Invalid scene state.");
				break;
			}
		}
	}
}

表 9. 面向该示例应用的触控处理程序和状态机。

“场景感知”特性可生成多个小网格。 这些网格通常包含不到 30 个顶点。 顶点的位置容易受到偏差的影响,因此有的网格会与它驻留的表面形成不同的角度。 如果对象位于网格顶部(比如目标或代理),该对象将位于错误的方向。 为解决该问题,我们使用网格的平均法线(表 10)。

private Vector3 AverageMeshNormal(Mesh mesh) {

	Vector3 sum = Vector3.zero;

	// Sum all the normals in the mesh
	for (int i = 0; i < mesh.normals.Length; i++){
		sum += mesh.normals[i];
	}

	// Return the average
	return sum / mesh.normals.Length;
}

表 10. 计算网格的平均法线。

构建应用

Github 提供适用于该示例的所有代码。

以下指令可将场景管理器和代理实施集成至英特尔® 实感™ 应用。

  1. 打开英特尔实感 SDK 文件夹“RSSDK\framework\Unity”中的“RF_ScenePerception”示例。
  2. 下载并导入 AutoNavAR Unity 文件包。
  3. 打开“Assets/AutoNavAR/Scenes/”文件夹中的“RealSenseExampleScene”。
  4. 在兼容英特尔实感摄像头 R200 的设备上构建并运行。

视频 4. 完成与英特尔® 实感™ 摄像头 R200 的集成。

原视频链接地址:https://www.youtube.com/watch?v=k-WwQwPgSIo

5. 有关自主导航的其他信息

我们开发了一个示例,展示如何在使用 R200 的增强现实应用中实施自主代理。 我们能够采用多种方式扩展该示例,从而增强代理的智能性和实施效果。

该代理拥有一种质量均匀并且没有方向移动限制的简单机械模型。 我们也可以开发质量分布不均并限制物体作用力(比如包含不同加速度和制动力的汽车、包含主推进器和侧推进器的宇宙飞船)的高级移动模型。 机械模型越精准,移动效果越真实。

Craig Reynolds 率先深入探讨了动画和游戏环境中的操控行为。 本示例所述的“靠近”、“抵达”和“避障”行为均源自他的作品。 Reynolds 还介绍了其他行为,包括离开、追逐、徘徊、探索、避障和路径跟随。 还探讨了分离、聚集、队列等组行为。 Mat Buckland 编写的《游戏人工智能编程案例精粹》同样介绍了如何实施这些行为,以及大量的相关概念(包括状态机和路径查找),非常实用。

在本示例中,“抵达”和“避障”操控行为可同时用于代理。 许多行为都可通过该方式协同工作,以创建更复杂的行为。 例如,可通过组合分离、聚集和队列来构建群集行为。 有时,组合行为可导致不太直观的结果。 我们可以试验不同类型的行为及参数,发现更多可能性。

另外,许多路径查找技巧都可用于动态环境。 D* 算法与 A* 类似,不过它能够根据最新的观察结果(比如添加/移除障碍)更新路径。 D* Lite的原理与 D* 相同,但实施方法更加简单。 通过设置坐标点和支持通过操控来导航这些坐标点,可以结合使用路径查找和操控行为。

本文不涉及行动选择,博弈论对其进行了详细介绍。 博弈论主要研究策略和决策制定背后的机制。 它包含涉及多个领域的应用,比如经济学、政治科学、心理学等等。 关于自主代理,博弈论会告知如何以及何时作出决策。 William Spaniel 编写的《博弈论: 完整教程》非常适合入门,并附带 YouTube 系列

6. 结论

您可以使用大量工具定制代理的移动、行为和行动。 自主导航特别适用于动态环境,比如增强现实应用中的英特尔实感摄像头所生成的环境。 即使采用简单的移动模型和操控行为,也可在不事先了解环境的情况下创建复杂行为。 大量可用的模型和算法有利于灵活实施面向几乎任何应用的自主解决方案。

关于作者

Ryan Measel 是 Fantasmo Studios的联合创始人兼首席技术官。 Ashwin Sinha 是创办团队成员和开发人员。 Fantasmo Studios 创办于 2014 年,是一家以提供混合现实应用内容和服务为主的技术娱乐公司。

利用 Skull Canyon 建造一台街机游戏机

$
0
0

大家好,我是 Bela Messex,是Buddy System(伙伴系统) 的一员。我们的工作室位于洛杉矶的公寓内,是 Little Bug 游戏的开发人员。

为什么要建造一台街机游戏机?

在我和我的联合开发人员所处的世界中,DIY 产品并不畅销,只是一种自然、必要的创新方式。 我和 Paul 相识以前,那时我们还没有共同设计视频游戏,我们制作交互式装置、杂志和漫画。 我们对数字游戏与物理交互的结合非常感兴趣,实现两者结合有多种形式,一种简单的方式是将我们的首款游戏 Little Bug 放置于定制的街机游戏机内。 事实证明,这样做非常轻松、有趣而且简单,在 Fantastic Arcade 和 Indiecade 等展会中,其独特的互动吸引了许多参会者。

规划

开始设计前,我利用 Image Effects、Animations 和完全失真的照明渲染了一个 Unity 设计。要是现实生活和视频游戏一样就好了,但至少现在我有了一个目标。

组件

尽管以下组件适用于我们的项目,也能帮助您开展自己的项目,但您可以根据游戏的独特要求来定制组件。

  • 英特尔 NUC Skull Canyon。
  • 2 个街机操作杆。
  • 3 个街机按钮。
  • 2 个通用 PC 操作杆主板(包括电线)。
  • 4 英寸 x 8 英寸 MDF 面板。
  • 24 英寸显示器。
  • 8 英寸 LED 强光灯。
  • 电源插板。
  • 电钻。
  • 射钉枪和木胶。
  • 各种型号的螺丝钉和弹簧。
  • 6 英寸琴式铰链。
  • 魔术贴扎带。
  • 束线带。
  • 黑色油漆和彩色油漆笔。
  • 半透明树脂玻璃。

建造游戏机

之前制作装置时,我主要负责焊接,所以我需要向 Paul 请教如何测量与切割 MDF 面板。 我们利用竖锯、铅笔和基本绘图工具当场设计形状,完成了测量与切割。 下图显示 Paul 在仓库工作室中建造即将完工的游戏机。

我们利用胶水和射钉枪将碎片组合在一起,如果您想要它承受更大的强度,可以使用螺丝钉固定。 请注意前面的铰链 - 这是 Paul 的想法,但是稍后在安装按钮和操作杆时,发挥了很大作用。 涂料旁边是脚踏板,由于 Little Bug 游戏需要同时使用两个操作杆和一个按钮,因此,我们专门针对这个独特的控制要求而设计了脚踏板。 双杆设置在游戏控制器上没有任何问题,但是转化成两个标准尺寸的街机操作杆时,两只手都占用了,如何按下按钮? 解决方法:用脚按按钮!

为整个框架喷漆后,终于进入了有趣的环节 - 安装电子部件。 我选择了较为经济(15 美元)的套件,包括 6 个按钮、一个操作杆、一个 USB 控制器和全部线缆。 运行了数百轮游戏后,它们仍旧表现良好。 请注意屏幕上面的 LED,能让字幕发光,制造经典街机游戏的感觉。

通过魔术贴扎带将 NUC 安装在游戏机后面之后,我在 Unity inspector 内同步了按钮和操作杆,建造了专为街机游戏机设计的全新构造。 Little Bug 采用手绘 sprite,因此,我们利用油漆笔绘制了所有的外部设计,以保持外观一致。 用油漆在涂漆用胶带上绘制模版,以此制作 Marquee。

街机游戏的乐趣

能够亲眼见证玩家与自己制作的游戏交互,这种乐趣是其他任何事情无法比拟的。 尽管 Little Bug 本质上没有变化,但是交互方式截然不同,看到玩家有了全新的游戏体验,我作为游戏设计人员感到非常激动。 NUC 体积轻巧、性能卓越,最适合创建此类的游戏体验,它的表现令人满意,我现在已经开始计划利用它设计更多的游戏。

面向 GEMM 引入新封装的 API

$
0
0

1     面向 GEMM 引入新封装的 API

矩阵-矩阵乘法 (GEMM) 是众多科学、工程和机器学习应用中的重要操作。优化这一操作的需求一直存在,英特尔® 数学核心函数库(英特尔® MKL)提供了并行的高性能 GEMM 实施。为了提供最佳性能,英特尔 MKL 提供的 GEMM 实施通常将原始输入矩阵转换为最适合目标平台的内部数据格式。数据转换(也被称为封装)会产生高昂的费用,尤其在处理具有 1 个或多个小尺寸的输入矩阵时。

英特尔 MKL 2017 引入了 [S,D]GEMM 封装应用编程接口 (API),该接口支持用户将矩阵明确转换为内部封装格式,以及将封装矩阵传输至多个 GEMM 调用。如果在 GEMM 调用之间重复使用输入矩阵(A 或 B),借助这种方法能在多个 GEMM 调用内分摊封装成本。

2      示例

以下 3 个 GEMM 调用使用了相同的 A 矩阵,但是每个调用使用了不同的 B/C 矩阵:

float *A, *B1, *B2, *B3, *C1, *C2, *C3, alpha, beta;

MKL_INT m, n, k, lda, ldb, ldc;

// initialize the pointers and matrix dimensions (skipped for brevity)

sgemm(“T”, “N”, &m, &n, &k, &alpha, A, &lda, B1, &ldb, &beta, C1, &ldc);

sgemm(“T”, “N”, &m, &n, &k, &alpha, A, &lda, B2, &ldb, &beta, C2, &ldc);

sgemm(“T”, “N”, &m, &n, &k, &alpha, A, &lda, B3, &ldb, &beta, C3, &ldc);

在每个 sgemm 调用中,A 矩阵被转换为内部封装数据格式。如果 n 较小(B/C 的列数),会导致 3 次封装矩阵的相对成本较高。通过单次封装 A 矩阵,并在 3 个连续的 GEMM 调用内使用经过封装的矩阵,可以最大限度地降低成本,其代码示例如下所示:

// allocate memory for packed data format

float *Ap;

Ap = sgemm_alloc(“A”, &m, &n, &k);

// transform A into packed format

sgemm_pack(“A”, “T”, &m, &n, &k, &alpha, A, &lda, Ap);

// SGEMM computations are performed using the packed A matrix:Ap

sgemm_compute(“P”, “N”, &m, &n, &k, Ap, &lda, B1, &ldb, &beta, C1, &ldc);

sgemm_compute(“P”, “N”, &m, &n, &k, Ap, &lda, B2, &ldb, &beta, C2, &ldc);

sgemm_compute(“P”, “N”, &m, &n, &k, Ap, &lda, B3, &ldb, &beta, C3, &ldc);

// release the memory for Ap

sgemm_free(Ap);

为了支持面向 GEMM 的封装 API,上述代码示例使用了 4 个新函数:sgemm_alloc、sgemm_pack、sgemm_compute 和 sgemm_free。首先,利用 sgemm_alloc 分配封装格式所需的内存,该函数接收一个确定封装矩阵(在本示例中为 A)的字符参数和三个表示矩阵尺寸的整数参数。然后,sgemm_pack 将原始 A 矩阵转换为封装格式 Ap 并执行 alpha 缩放。未更改原始 A 矩阵。三个 sgemm 调用被替换为三个 sgemm_compute 调用,后者处理封装矩阵并假设 alpha=1.0。输入 sgemm_compute 的前两个字符参数表示 A 矩阵为封装格式 (“P”),B 矩阵为非转置 column major 格式 (“N”)。最终,通过调用 sgemm_free 释放了分配给 Ap 的内存。

从本示例可以看出,GEMM 封装 API 消除了在 3 个矩阵-矩阵乘法操作中两次封装矩阵 A 的成本。如果在多个 GEMM 调用之间重复使用 A 和/或 B 输入矩阵,封装 API 可以消除该输入矩阵的数据转换成本。

3      性能

下表显示了利用封装 API 在英特尔® 至强融核™ 处理器 7250 上取得的性能提升。假设如果使用相同的 A 矩阵的 SGEMM 调用数量很多,可以完全分摊封装成本。本文还提供了普通 SGEMM 调用的性能,以进行对比。

4      实施要点

建议利用相同数量的线程调用 gemm_pack 和 gemm_compute,以达到最佳性能。需要指出的是,如果使用相同的 A 或 B 矩阵的 GEMM 调用数量较少,封装 API 可能提供微乎其微的性能优势。

gemm_alloc 例程分配的内存与原始输入矩阵的规模相当。这意味着对于大型输入矩阵,应用对内存的要求将显著增大。

在英特尔 MKL 2017 中,只面向 SGEMM 和 DGEMM 实施 GEMM 封装 API。它们适用于所有的英特尔架构,但是只面向 64 位英特尔® AVX2 及更高版本进行了优化。

5      结论

[S,D]对于使用相同输入矩阵的多个 GEMM 调用,利用 GEMM 封装 API 能够最大限度地降低数据封装成本。如性能图表所示,如果在多个 GEMM 调用中矩阵得到充分地重复使用,调用它们能显著提升性能。可以在英特尔 MKL 2017 中获取这些封装 API,支持 FORTRAN 77 和 CBLAS 两种接口。如欲获取更多文档,请查看英特尔 MKL 开发人员参考。

 

Optimization Notice in English

NetUP 利用英特尔® 媒体软件开发套件向全球数百万观众转播里约奥运会赛事

$
0
0

2016 年 8 月,全球各地的 50 万粉丝来到里约热内卢,观看为期 17 天的夏季奥运会。 同时,全球数百万观众在电视机屏幕前观看了奥运会直播。

在不同的大陆上进行实况电视转播是一项艰巨的任务,需要可靠的设备和灵活的技术支持。 对全球最大的多媒体新闻机构 - 汤森路透 (Thomson Reuters) 来说,这是个不小的挑战。

为了迎接挑战,汤森路透选择了 NetUP 作为技术合作伙伴,使用 NetUP 设备进行了从里约热内卢到纽约与伦敦办事处的实况转播。 NetUP 与英特尔携手开发了 NetUP 转码器,使用了英特尔® 媒体软件开发套件,该套件是一款跨平台的 API,用于在 Windows* 上开发媒体应用。

”这个项目对我们来说非常重要,“ NetUP 创始人 Abylay Ospan 说,”它展示了我们开发的解决方案的高品质和可靠性,可以用于转播全球盛事,如奥运会。 英特尔媒体软件开发套件支持快速转码,确保全球观众都能够收看奥运会转播。“

请访问最新的案例研究以了解全部详情。

快速的 ISPC 纹理压缩工具 - 更新

$
0
0

下载代码示例

本文和示例代码项目由英特尔公司的 Marc Fauconneau Dufresne 撰写。

2016 年 4 月 12 日更新
本次更新添加了面向 RGBA ASTC 压缩的支持。

2015 年 8 月 26 日更新
本次更新在快速的 ISPC 纹理压缩示例中添加了高质量的 ETC1 和 ASTC 压缩。对于 ASTC 压缩,目前只支持 RGB 2D LDR 输入。支持最高 8x8 的模块规格(而非 10x5 或 10x6)。利用 英特尔 SPMD 编译器启用 SIMD 指令集。下图展示了在带有 6x6 模块的 Kodak 数据集上,astcenc 与快速 ISPC 纹理压缩工具的性能与质量关系的对比。在快速预置方面与 astcenc 不相上下,但是速度提升了 44 倍。


图 1 - 快速 ISPC 纹理压缩工具与 astcenc 之间的性能与质量对比需要注意的是,x 轴运用了对数尺度。

 

2014 年 5 月 13 日更新
本示例在 一流的 BC7 纹理压缩工具上 添加了 BC6H(DX11 HDR 纹理格式)支持。我们采用了相似的方法进行快速分区过滤、选择和修改,还采用了高效的搜索策略,以充分利用 B6CH 端点转换。利用 英特尔 SPMD 编译器启用了 SIMD 指令集。提供了质量与性能之间的各种关系。


图 2ISPC 纹理压缩工具更新 - 2014 年 5 月 13 日

原始版本(2013 年 11 月 5 日)
本示例展示了一流的 BC7 (DX11) 纹理压缩工具。BC7 分区被简化为若干阶段,利用迭代端点修改优化了最终的分区。支持所有 BC7 模式。借助 英特尔 SPMD 编译器启用了 SIMD 指令集。提供了质量与性能之间的各种关系。


图 3 - 原始 ISPC 纹理压缩工具 - 2013 年 11 月 5 日


英特尔® Software Guard Extensions 教程系列:第一部分,英特尔® SGX 基础

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)教程系列的第一部分仅对该技术进行了简单概述。 更多详情,请参阅英特尔 Software Guard Extensions SDK提供的相关资料。 在文章介绍英特尔® Software Guard Extensions 教程系列中查找本系列所有教程列表。

了解英特尔® Software Guard Extensions 技术

软件应用通常需涉及诸如密码、账号、财务信息、加密秘钥和健康档案等私人信息。 这些敏感数据只能由指定接收人访问。 按英特尔 SGX 术语来讲,这些隐私信息被称为应用机密。

操作系统的任务对计算机系统实施安全策略,以避免这些机密信息无意间暴露给其他用户和应用。 操作系统会阻止用户访问其他用户的文件(除非访问已获得明确许可),阻止应用访问其它应用的内存,阻止未授权用户访问操作系统资源(除非通过受到严格控制的界面进行访问)。 应用通常还会采用数据加密等其它安全保护措施,以确保发送给存储器或者通过网络连接而发送的数据不会被第三方访问到——即使是在操作系统和硬件发盗用的情况下。

尽管有这些措施提供保护,但大部分计算机系统仍然面临着一项重大安全隐患:虽然有很多安保措施可保护应用免受其它应用入侵,保护操作系统免受未授权用户访问,但是几乎没有一种措施可保护应用免受拥有更高权限的处理器的入侵,包括操作系统本身。 获取管理权限的恶意软件可不受限制地访问所有系统资源以及运行在系统上的所有应用。 复杂的恶意软件可以锁定应用的保护方案为目标进行攻击,提取加密秘钥,甚至直接从内存提取机密数据。

为对这些机密信息提供高级别的保护,同时抵御恶意软件的攻击,英特尔设计了英特尔 SGX。 英特尔 SGX 是一套 CPU 指令,可支持应用创建安全区: 应用地址空间中受保护的区域,它可确保数据的机密性和完整性——即便有获取权限的恶意软件存在。 安全区代码可通过专用指令启用,并被构建和加载成 Windows* 动态链接库 (DLL) 文件。

英特尔 SGX 可减少应用的攻击面。 图 1 显示了借助英特尔 SGX 安全区和不借助英特尔 SGX 安全区时,攻击面的显著差异。

Attack-surface areas with and without Intel® Software Guard Extensions enclaves.

图 1借助和不借助英特尔® Software Guard Extensions 安全区时的攻击面。

英特尔 Software Guard Extensions 技术如何确保数据安全

英特尔 SGX 可针对已知的硬件和软件攻击提供以下保护措施:

  • 安全区内存不可从安全区外读写,无论当前的权限是何种级别,CPU 处于何种模式。
  • 产品安全区不能通过软件或硬件调试器来调试。 (可创建具有以下调试属性的安全区:该调试属性支持专用调试器,即英特尔 SGX 调试器像标准调试器那样对其内容进行查看。 此措施旨在为软件开发周期提供辅助)。
  • 安全区环境不能通过传统函数调用、转移、注册操作或堆栈操作进入。 调用安全区函数的唯一途径是完成可执行多道保护验证程序的新指令。
  • 安全区内存采用具有回放保护功能的行业标准加密算法进行加密。 访问内存或将 DRAM 模块连接至另一系统只会产生加密数据(见图 2)。
  • 内存加密秘钥会随着电源周期(例如,启动时或者从睡眠和休眠状态进行恢复时)随机更改。 该秘钥存储在 CPU 中且不可访问。
  • 安全区中的隔离数据只能通过共享安全区的代码访问。

受保护的内存在大小上存在硬限值,该限值由系统 BIOS 设定,通常为 64 MB 和 128 MB。 有些系统提供商可能会将此限值制定成其 BIOS 设置内的可配置选项。 内存中可同时保留 5 到 20 个安全区,这取决于每个安全区的大小。

How Intel® Software Guard Extensions helps secure enclave data in protected applications.

图 2英特尔® Software Guard Extensions 如何确保受保护应用中安全区数据的安全。

设计注意事项

包含英特尔 SGX 技术的应用设计要求将应用分成两个部分(见图 3):

  • 可信部分。它指的是安全区。 可信代码中的代码是访问应用机密的代码。 一款应用可以拥有一个以上可信部分/安全区。
  • 不可信部分。它包括应用的剩余部分及其所有模块。 需要指出的是,从安全区的角度来看,操作系统和虚拟机显示器都被看做不可信部分。

可信部分应尽量保持最小,仅限于需要最高等级保护的数据以及必须直接作用于其上的操作。 具有复杂界面的大型安全区不仅仅会消耗更多受保护内存:还会产生更大攻击面。

安全区还应使可信-不可信部分交互程度保持最低。 虽然安全区可离开受保护内存区域,在不可信部分(通过专用指令)调用函数,但对依赖性进行限制将会针对攻击对安全区进行加固。

Intel® Software Guard Extensions application execution flow.

图 3英特尔® Software Guard Extensions 应用执行流程。

认证

在英特尔 SGX 架构中,认证指的是以下流程,即证明在平台上建立了特定安全区。 有两种认证机制:

  • 本地认证 适用于同一平台上两个安全区进行相互认证时。
  • 远程认证 适用于某一安全区获取远程提供商的信任时。

本地认证

当应用拥有一个以上需要相互协作才能完成任务的安全区时,或者两款应用必须在安全区之间进行数据通信时,本地认证非常有用。 每个安全区都须对另一安全区进行验证,以确认双方都是可信安全区。 一旦完成认证,它们就会建立受保护会话,采用 ECDH Key Exchange 共享会话秘钥。 该会话秘钥可用于对必须在这两个安全区之间进行共享的数据进行加密。

因为一个安全区不能对另一个安全区的受保护内存空间进行访问——即使它们运行在同一应用中,所以必须将所有指针解除引用至其值和副本,且必须将完整的数据集从一个安全区封送至另一安全区。

远程认证

借助远程认证,英特尔 SGX 软件和平台硬件的组合可用于生成评价,评价会被发送至第三方服务器以建立信任。 该软件包括应用安全区,以及评价安全区 (QE) 和配置安全区 (PvE),后两者皆由英特尔提供。 该认证硬件是英特尔 SGX 所支持的 CPU。 将该软件信息摘要与来自该硬件的平台唯一非对称秘钥相组合以生成评价,再通过已认证渠道将评价发送至远程服务器。 如果远程服务器确认安全区得到了正确实例化且运行在真正支持英特尔 SGX 的处理器上,远程服务器就会立即信任该安全区并选择通过已认证渠道向其提供机密信息。

密封数据

密封数据是对数据进行加密,以便可在不泄露其内容的前提下将数据写至不可信内存或对其进行存储。 稍后,该安全区可将该数据读回并进行解封(解密)。 加密秘钥由内部按需推导,不会暴露给安全区。

有两种数据密封方法:

  • 安全区标识。本方法可生成一个所述安全区所独有的秘钥。
  • 密封标识。本方法可生成一个基于安全区密封授权方标识的秘钥。 相同签名授权方的多个安全区可推导出相同秘钥。

密封至安全区标识

密封至安全区标识时,秘钥对于密封数据的具体安全区是独一无二的,该安全区会影响其签名的任何更改都会产生新秘钥。 借助该方法,使用一版安全区密封的数据不可由其他版本的安全区访问,因此该方法的一个副作用是,密封的数据不可迁移至较新版本的应用及其安全区中。 它专为密封的旧数据不应被新版应用使用的应用而设计。

密封至密封标识

密封至密封标识时,来自同一授权方的多个安全区可透明地对彼此的数据进行密封和解封。 这样,来自一版安全区的数据可迁移至另一版,或在同一软件厂商的多个应用中进行共享。

如果需要防止旧版软件和安全区访问较新应用版本密封的数据,授权方可在对安全区进行签名时写下软件版本编号 (SVN)。 比指定 SVN 更旧的安全区版本将不可推导出密封密钥,因此不可对数据进行解封。

我们将如何在本教程中使用英特尔 Software Guard Extensions 技术

我们描述了英特尔 SGX 的三大要素:安全区、认证和密封。 对于本教程,我们将重点实施安全区,因为安全区是英特尔 SGX 的核心。 如果不先建立安全区,您不可进行认证或密封。 这还有助于确保教程保持可管理大小。

即将推出

本教程系列第二部分,英特尔® Software Guard Extensions 教程系列: 第二部分,应用设计,将聚焦我们将为英特尔 SGX 构建和支持的密码管理程序应用。 我们将谈及设计要求、限制和用户界面。 敬请关注!

 在文章介绍英特尔® Software Guard Extensions 教程系列中查找本系列所有教程列表。

 

英特尔® Software Guard Extensions 教程系列:第二部分,应用设计

$
0
0

英特尔® Software Guard Extensions(英特尔® SGX)教程系列的第二部分介绍了我们将开发的应用(简单的密码管理程序)的高级规格。 全新的设计工作支持我们从一开始专为英特尔 SGX 而构建。 这意味着,除说明应用要求,我们还将分析英特尔 SGX 设计决策和整个应用架构会如何相互影响。

阅读本系列第一部分教程,或介绍英特尔® Software Guard Extensions 教程系列的文章中查找所有已发表教程的列表。

密码管理程序一览

多数人可能熟悉密码管理程序及其功能,但在详细了解应用设计之前有必要回顾其基本信息。

密码管理程序的主要目标是:

  • 减少最终用户需要记住的密码数量。
  • 帮助最终用户创建安全性更高的密码 —— 相比其通常选择的密码。
  • 确保针对每个账户使用不同密码具有实用性。

密码管理日益成为互联网用户的问题。多年来,多项研究在努力对这一问题进行量化。 Microsoft 于 2007 年发布的一项调查—— 距离本文写作时接近 10 年 —— 估计,普通人拥有 25 个需要密码的账户。 2014 年,Dashlane 估计其美国用户平均拥有 130 个账户,而其全球用户所拥有账户的平均数量为 90。 问题并未就此结束:用户都不善于挑选“好”密码(众所周知),经常在多个站点重复使用相同密码,这已经导致了一些备受关注的攻击事件。 这些问题基本可归为两类:黑客工具难以猜透的密码通常也不易于用户记住,以及密码数量的增多让问题更加复杂,因为用户需要花费更多心思记住哪个密码与哪个账户相关联。

借助密码管理程序,您只需记住一个安全性极高的密码,便可访问密码数据库。 通过了密码管理程序的身份验证之后,您便可查找您存储的任何密码,并根据需要将其复制粘贴在身份验证字段中。 当然,密码管理程序的主要弱点恰恰在于密码数据库本身:由于包含所有的用户密码,因此它对于攻击者极具诱惑力。 因为这个原因,密码数据库需使用安全性较高的加密技术进行加密,这样解密其中的数据就需要用户的主密码。

我们制作本教程的目标是帮助构建一个简单的密码管理程序 —— 它能够在遵守良好安全实践的情况下提供与商用产品相同的核心功能,并将其用作针对英特尔 SGX 进行设计的学习工具。 教程密码管理程序(我们称之为“支持英特尔® Software Guard Extensions 的教程密码管理程序”,名字虽然有点长,但具有描述性)并非要用作商用产品,因此不会、也没必要包含所有保护功能。

基本的应用要求

一些基本的应用要求将可帮助缩小应用的范围,以便我们专注于英特尔 SGX 集成,而非应用设计和开发的细节。 再次强调,我们的目标不是创建商用产品,支持英特尔 SGX 的教程密码管理程序无需在多个操作系统或所有可能的 CPU 架构上运行。 我们只需要一个合理的起点。

为此,我们的基本应用要求是:

第一个要求可能有点奇怪,因为本教程系列旨在说明英特尔 SGX 应用开发,但实际的应用需要考虑传统的安装基础。 对于一些应用而言,将执行范围限制于支持英特尔 SGX 的平台的范围内可能比较合适,但对于教程密码管理程序,我们将放宽要求。 支持英特尔 SGX 的平台将支持强化的执行环境,但不支持的平台仍可正常运行。 这种使用方法适用于密码管理程序。使用密码管理程序时,用户可能需要将其密码数据库与其他较旧的系统进行同步。 它还是实施双代码路径的学习机会。

第二个要求是访问非英特尔 SGX 代码路径中的某些密码算法以及我们需要的一些库。 64 位要求通过确保原生 64 位类型的访问简化了应用开发,而且提升了某些针对 64 位代码而优化的密码算法的性能。

第三个要求是访问非英特尔 SGX 代码路径中的 RDRAND 指令。 这可极大简化随机数生成,确保对高质量熵源的访问。 支持 RDSEED 指令的系统也可利用它。 (有关 RDRAND 和 RDSEED 指令的信息,请访问英特尔® 数字随机数生成器软件实施指南)。

第四个要求是尽量减少开发人员(和最终用户)需要的软件数量。 无需下载和安装第三方库、框架、应用或实用程序。 然而,该要求有一个不利的副作用:没有第三方框架,我们只可使用四个选项创建用户界面。 这些选项包括:

  • Win32 API
  • Microsoft 基础类 (MFC)
  • Windows Presentation Foundation (WPF)
  • Windows Forms

前两个选项使用原生/非托管代码实施,后两个需要使用 .NET*。

用户界面框架

对于教程密码管理程序,我们在 C# 中使用 Windows Presentation Foundation 开发 GUI。 该设计决策会影响我们的下列要求:

为何使用 WPF? 主要是因为它可简化 UI 设计,并会带来我们实际上需要的复杂性。 具体而言,通过使用 .NET 框架,我们可以讨论将托管代码、特别是高级语言与安全区代码进行混合。 不过,请注意,WPF 和 Windows Forms 都可选择,两种环境都能有效运行。

您可能记得,安全区必须使用原生 C 或 C++ 代码编写,与安全区交互的桥函数必须为原生 C(而非 C++)函数。 尽管 Win32 API 和 MFC 支持使用完全原生的 C/C++ 代码开发密码管理程序,但这两种方法不会对希望学习英特尔 SGX 应用开发的人员造成负担。 借助基于托管代码的 GUI,我们不仅可获得集成设计工具的优势,而且可讨论一些可为英特尔 SGX 应用开发人员带来潜在价值的功能。 简言之,您并非要学习 MFC 或原始 Win32,但可能希望了解如何将 .NET 粘合至安全区。

为将托管和非托管代码连接起来,我们将使用 C++/CLI(针对通用语言基础设施而修改的 C++)。 它可大幅简化数据封送,而且因其便捷性和易用性被许多开发人员形容为 IJW(“确实有效”)。

图 1原生和 C# 英特尔® Software Guard Extensions 应用的最小组件结构。

图 1 所示为英特尔 SGX 应用从原生代码迁移至 C# 时其最小组件结构受到的影响。 在完全原生的应用中,应用层可直接与安全区 DLL 交互,因为安全区桥函数可纳入应用的可执行文件。 然而,在混合模式应用中,安全区桥函数需要与托管代码块相隔离,因为它们需要是完全原生的代码。 另一方面,C# 应用不可直接与桥函数交互,在 C++/CLI 中这意味着创建另一个媒介:在托管 C# 应用和原生安全区桥 DLL 之间封送数据的 DLL。

密码库要求

密码数据库或我们所称的密码库是密码管理程序的核心。 这种加密文件将保存最终用户的账户信息和密码。 我们教程应用的基本要求是:

密码库应可移植的要求意味着我们应在将密码库文件复制到另一台计算机后仍可访问其内容,无论它们是否支持英特尔 SGX。 换言之,用户体验应该一致:密码管理程序应该无缝运行(当然,前提是系统满足基础硬件和操作系统要求)。

加密静态密码库意味着密码库文件在未使用时应被加密。 密码库至少须在磁盘上进行加密(若没有可移植性要求,我们有望使用英特尔 SGX 的封闭特性满足加密要求),且若无必要,在内存中不应处于未加密状态。

经过验证的加密可确保加密的密码库在加密之后未被修改。 它还可帮助我们方便地验证用户的密码:如果解密密钥不正确,在对验证标签进行验证时解密会失败。 这样,我们无需检测解密数据是否正确。

密码

从多方面考虑,任何账户信息都是敏感信息,它可准确告知攻击者将攻击目标锁定哪些登录名和站点,不过密码无疑是密码库最重要的要素。 确定攻击目标账户后就可发起攻击。 为此,我们将介绍有关密码库中所存储密码的更多要求:

嵌入加密功能。 每个用户账户的密码存储在密码库时经过了加密,整个密码库在写入磁盘时进行了加密。 该方法支持我们在密码库解密后避免密码被曝光。 对密码库进行全面解密可帮助用户浏览其账户详情,但以这种方式清晰显示所有密码文本则并不合适。

账户密码只在用户请求查看它时被解密。 这可防止密码在内存中和用户显示屏上被曝光。

密码算法

确定加密需求后,我们需选择特定的密码算法。根据我们的现有应用要求,相关选项受到一些严格限制。 该教程密码管理程序必须在英特尔 SGX 和非英特尔 SGX 平台上提供无缝用户体验,不可依赖第三方库。 这意味着我们需要选择一种算法以及支持的密钥和验证标签大小,这对于 Windows CNG API 和英特尔 SGX 可信密码库都一样。 实际上,我们只有一个选项: 高级加密标准-Galois 计算器模式 (AES-GCM),带有 128 位密钥。 这可以说是该应用中可使用的最佳加密模式,尤其是因为 128 位 GCM 的有效验证标签强度小于 128 位,但它足以我们的目的。 请记住:我们的目标不是创建一款商用产品,而是提供一款实用的英特尔 SGX 开发学习工具。

选择 GCM 时,我们同时做出了一些其他的设计决策,包括 IV 长度(12 字节最适合该算法)和验证标签。

加密密钥和用户身份验证

选择加密方法后,我们可将关注点转向加密密钥和用户身份验证。 用户如何在密码管理程序中进行身份验证并解锁密码库?

简单的方法是直接使用密钥推导函数 (KDF) 从用户密码中推导加密密钥。 不过,这个简单的方法虽然有效,却存在一个缺点:如果用户修改了其密码,加密密钥也会随之改变。 因此,我们将采用更为常见的做法:对加密密钥进行加密。

在该方法中,主加密密钥使用高质量熵源随机生成,绝不会变化。 用户密码用于推导辅助加密密钥,后者用户对主密钥进行加密。 这种方法具备一些重要优势:

  • 当用户密码改变时,数据无需再次加密
  • 加密密钥绝不会改变,因此理论上它能以十六进制表示法进行记录,锁定在物理安全的位置。 即使当用户忘记其密码时,数据仍可解密。 由于密钥绝不会改变,因此它只需记录一次。
  • 理论上,这种方法满足用户访问数据的需求绰绰有余。 每位用户可使用自己的密码加密主密钥的副本。

所有这些措施并非都需要用于教程密码管理程序或与其相关,但构成了一种有效的安全实践。

主密钥被称为密码库密钥,通过用户密码推导的辅助密钥被称为总密钥。 用户可输入密码进行身份验证,密码管理程序可从中推导出总密钥。 如果总密钥成功解密密码库密钥,用户则通过了身份验证,密码库被解密。 如果密码不正确,密码库密钥解密失败,因此密码库不可解密。

最后一个要求,即根据 SHA-256 构建 KDF,源于我们发现的一项限制,即哈希算法同时支持 Windows CNG API 和英特尔 SGX 可信密码库。

帐户详情

最后一个高级要求与在密码库中实际存储什么内容相关。 对于该教程,我们将力求简单。 图 2 所示为主 UI 屏幕的早期模型。

图 2:教程密码管理程序主屏幕的早期模型

最后一个要求与简化代码相关。 通过固定密码库中存储账户的数量,我们可更轻松地确定密码库大小的上限。 这在我们开始设计安全区时至关重要。 当然,实际的密码管理程序不具备这一优势,但它可满足本教程的目的。

即将推出

本教程第三部分,我们将详细探讨如何针对英特尔 SGX 设计教程密码管理程序。 我们将确定机密(应用的这一部分应包含在安全区内)、安全区如何与核心应用交互以及安全区如何影响对象模型。 敬请关注!

阅读本系列第一部分教程英特尔® Software Guard Extensions 教程系列: 第一部分,英特尔® SGX 基础,或在介绍英特尔® Software Guard Extensions 教程系列文章中查找所有已发表教程的列表。

 

Caffe学习笔记 第一部分 - Windows*下BVLC Caffe的安装与配置

$
0
0

作者:Gu, Jianjun

Tensorflow和Caffe是机器学习初学者常用的2种深度学习框架。相对于Tensorflow有简单的python pip一键安装包,Caffe的安装更考验开发者的计算机编译水平,需要自己下载源码编译。尽管caffe的配置教程网上很多,但是网上caffe的各个分支版本也很多,所以初学者在编译时总是会碰到各种奇怪的问题。本文会针对初学者常用的Windows* + Intel CPU的平台上安装配置Caffe做一个较为完整的总结。

Caffe的各个版本简介

官方版本:伯克利BVLC(Berkeley Vision And Learning Center) 版

https://github.com/BVLC/caffe
这个是Caffe的主版本,由伯克利大学维护。其他所有的Caffe版本都是由这个版本分支出去的。这个版本Caffe可以编译出Linux和Windows*版本,支持Nvidia的CUDA加速,但是对CPU的优化不好,而且有关安装和配置的文档很少。从这个版本编译Caffe,需要很强的自我学习和解决问题能力。

微软的Windows*版本

https://github.com/Microsoft/caffe
微软自己维护的一个版本。这个版本简化了Caffe在Windows*下的步骤,自带了一个VS的Solution项目。这个版本同样支持基于CPU和CUDA的算法实现。

Intel Caffe优化版

https://github.com/intel/caffe
Intel维护的版本,这个版本优化了Intel CPU的实现算法,同时提供对Intel Xeon CPU多核和多节点的支持。这个版本里有些算法库对Windows*支持不太好,所以这个分支编译出的Caffe主要还是运行在Linux的服务器平台上做训练工作。

Intel clCaffe 核显OpenCL优化版

https://github.com/01org/caffe
Intel维护的另一个版本,相对于官网BVLC Caffe上的OpenCL分支,优化了基于Intel 核显GPU的OpenCL加速算法。这个版本同时支持Windows* 和Linux平台,适合在有Intel核显的CPU上做一些推理(Inference)场景的工作。

BVLC caffe的编译

安装编译所需要的软件

配置Windows*的环境变量

为了避免等会CMAKE生成编译脚本的时候找不到一些依赖关系,有的没的路径都加一些,包括Cmake, Git, Ananconda以及Python的路径。

Checkout 官网Caffe的代码

运行以下代码,用git从官网caffe上下载Windows*分支。

git clone https://github.com/BVLC/caffe.git
cd caffe
git checkout windows

修改caffe源码代码里的一些编译参数

  • 修改scripts\build_win.cmd
    因为我们没有定义APPVEYOR,所以直接拉到else(大约69行)以后。
    先从APPVEYOR部分把这部分内容拷贝过来
if !PYTHON_VERSION! EQU 2 (
    set CONDA_ROOT=C:\Miniconda-x64
)
:: Set python 3.5 with conda as the default python
if !PYTHON_VERSION! EQU 3 (
    set CONDA_ROOT=C:\Miniconda35-x64
)
set PATH=!CONDA_ROOT!;!CONDA_ROOT!\Scripts;!CONDA_ROOT!\Library\bin;!PATH!

然后根据Windows*环境设置下图中红色方框的部分

1. 编译器是VS2015,设置MSVC_VERSION=14

2. 不需要NINJA来编译,设置WITH_NINJA=0

3. 没有NVDIA GPU,设置CPU_ONLY=1

4. Python版本为3.X,设置PYTHON_VERSION=3

5. 需要pyCaffe支持,设置BUILD_PYTHON=1

  • 如果Python版本不是2.7或3.5,修改
    cmake\WindowsDownloadPrebuiltDependencies.cmake
    Caffe在编译时会自动从网上下载一个依赖库caffe-builder,这个cmake文件负责根据你系统里的VS版本和python版本自动下载预编译好的caffe-builder包。目前网上只有预编译好的VS2013/2015配python2.7/3.5的caffe-builder,如果是python3.6,编译时会报找不到VS2015配python3.6的caffe-builder包。解决方法有2个办法,要么用python3.5的包,要么自己编译caffe-builder。

    对于复用Python3.5的包,可以按照下图中所示,复制DEPENDENCIES_URL_1900_35和DEPENDENCIES_SHA_1900_35 2行,同时修改35为36。

如果希望使用自己编译VS2015加python3.6的caffe-builder,除了要加入上段说到的2行修改外,还需要注释掉下图中WindowsDownloadPrebuiltDependencies.cmake的下图所示的绿色部分,这部分代码是负责从网上下载依赖包并且解压缩到本地目录的。同时要把caffe-builder编译出的libraries目录拷贝到C:\Users\[user name]\.caffe\dependencies这个目录下。(具体caffe-builder的编译,将在本文第3节详细介绍)

开始编译

在caffe的目录下输入scripts\build_win.cmd

开始编译,一切顺利的话,大约10分钟后就编译好了

验证一下编译的结果

接下来运行一下caffe项目自带的examples里的00-classification的代码来验证一下caffe是否能够正常运行

打开anaconda的命令行,进入caffe的examples目录,运行jupyter notebook

在打开的notebook中打开caffe自带的例子 00-calssification.ipynb

这是用一个训练好的Caffe模型来预测动物图片的例子,图片默认是使用Caffe项目里examples\image\cat.jpg。

一路Shift+Enter运行下去,看到第8步predicted输出

predicted class is: 281

第9步输出

output label: n02123045 tabby, tabby cat

预测结果是猫,说明caffe已经正确编译而且能运行了。 

大功告成,开始你的Caffe学习之旅吧

Caffe依赖库Caffe Builder的编译

这一章主要介绍怎么编译Caffe-Builder项目

安装编译所需要的软件

编译软件的需求同2.1部分

配置Windows*的环境变量

环境变量的配置同2.2部分

下载Caffe-Builder源码

Caffe-Builder的开源项目位于 https://github.com/willyd/caffe-builder
目前最新的release为1.1版,可以从这里下载最新的release 1.1.0版的源码
https://github.com/willyd/caffe-builder/releases

修改caffe-builder源码代码里的一些编译参数

修改主要基于2个方面:
首先在Windows*下Ninja编译系统不容易配置好,所以这里选择了Visual Studio 2015作为编译器。修改build_v140_x64.cmd,将红色部分的’Ninja’改为’Visual Studio 14 2015 Win64’

其次是这个Release 1.1.0发布有点时间了,caffe-builder很多依赖的开源项目都搬家或者版本更新了,需要做相应的修改。

1) 修改packages\protobuf\CmakeLists.txt
将图中所示部分protobuf包的Hash值从原来的
14a532a7538551d5def317bfca41dace
修改为
39d6a4fa549c0cce164aa3064b1492dc

2) 修改packages\hdf5\CmakeLists.txt
将图中所示部分的URL从https://www.hdfgroup.org/ftp/HDF5/releases/hdf5-1.8.16/src/CMkae-hdf5-1.8.16.zip修改为 https://www.hdfgroup.org/ftp/HDF5/releases/hdf5-1.8/hdf5-1.8.16/src/CMkae-hdf5-1.8.16.zip

编译

进到caffe-builder-1.1.0目录下,运行build_v140_x64.cmd开始编译

屏幕输出

最终编译生产的caffe依赖库文件放在build\libraries目录下,你可以将这个libraries目录拷贝到caffe所需要的目录下。

小结

本篇文章主要介绍了官方BVLC Caffe在Windows*下的编译设置过程。BVLC Caffe提供CPU和Nvidia GPU版本的实现,但是在实际学习工作中,初级开发者的电脑平台通常不会装备昂贵的Nvidia显卡,而CPU版本的Caffe因为执行效率不高,只能用来做一些小型的”玩具”实验项目。

下一篇文章,会介绍一种基于Intel集成GPU核显加速的clCaffe框架。通过 clCaffe框架,开发者可以在中小型项目开发中在硬件成本和产品性能之间找到一个很好的平衡点。

Caffe学习笔记 第二部分 - Windows* 下基于Intel核显加速的clCaffe的安装,配置与性能提升

$
0
0

作者:Gu, Jianjun

点击访问Caffe学习笔记 第一部分 - Windows*下BVLC Caffe的安装与配置

clCaffe编译与配置

Intel clCaffe (https://github.com/01org/caffe)是利用基于Intel Skylake及以后的处理器核显(即Gen9架构以上)做硬件加速的一个修改版。如果你当前机器是基于Nvdia显卡,请用NV cuda加速版本;如果你的显卡是AMD的,请check out 官方BVLC caffe的opencl分支。

clCaffe的编译

安装编译所需要的软件

配置Windows*的环境变量

为了避免等会CMAKE生成编译脚本的时候找不到一些依赖关系,有的没的路径都加一些,包括Cmake, Git, Ananconda以及Python的路径。(环境变量的配置同上篇文章的2.2部分 )

下载和编译Intel clCaffe工程

clCaffe的编译过程和caffe基本类似。不同的是clCaffe所需的caffe-builder的libraries目录是放在clCaffe自己目录的build目录下,同时额外多下载编译了一些支持openCL运行的开源项目。为了简单起见,这里参考了一个开源项目里的编译脚本https://github.com/liyuming1978/caffe_example/blob/master/install_scripts/Windows_install/build-clcaffe.cmd

首先先创建一个clcaffe-windows的目录,下面提供了一个简单的编译脚本build-clcaffe.cmd(为了简化clCaffe的编译过程,这里直接提供了完整的编译脚本,不再解释脚本里每一步的具体目的,有兴趣的开发者可以自己研究修改脚本来满足自己的需求),在clcaffe-windows目录下创建并且执行这个批处理脚本文件。这个脚本是根据我自己的环境 Win10+VS2015+Python3.6写的,如果你的开发环境跟我的不同,比如是python2.7或者3.5,需要按照脚本里的注释做相应的修改。

@echo off
@setlocal EnableDelayedExpansion

echo must install openclsdk,python(anaconda),git,cmake,vs 2015 for desktop

::设置python所在路径, 基于python3,
::如果编译环境是python2, 需要修改下面print()这句话,按照python2的语法格式修改
for /f "delims=" %%t in ('python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"') do set py_path_str=%%t

::下载并编译clCaffe所需要的一些依赖项目
cd %~sdp0
git clone https://github.com/dlfcn-win32/dlfcn-win32
cd dlfcn-win32
cmake -G "Visual Studio 14 2015 Win64" .
cmake --build . --config Release
cd %~sdp0
::git clone https://github.com/ptillet/isaac.git (isaac build wrong, please use intel isaac)
git clone https://github.com/intel/isaac.git
cd isaac
mkdir build
cd build
cmake -G "Visual Studio 14 2015 Win64" ..
cmake --build . --config Release

::下载clCaffe的项目
cd %~sdp0
git clone https://github.com/01org/caffe.git
cd caffe
git checkout inference-optimize
git pull
git clone https://github.com/viennacl/viennacl-dev.git

::下面这部分是拷贝自己编译的caffe-builder的libraries到clCaffe的目录里,同时编译时需要
::修改WindowsDownloadPrebuiltDependencies.cmake,注释掉对应的网络下载和解压缩代码
::防止WindowsDownloadPrebuiltDependencies.cmake脚本报找不到网上对应的caffe-builder包,
:: 如果编译环境是python2.7或者3.5, 可以注释掉下面这段代码, 编译脚本会自动从网上下载预编译好的依赖库
cd %~sdp0
cd caffe
mkdir build
cd .\build
mkdir libraries
cd ..
xcopy C:\work\caffe-builder-1.1.0\build_v140_x64\libraries .\build\libraries /s /h /c /y

::设置编译参数,开始编译clCaffe
cd %~sdp0
cd caffe
set BUILD_PYTHON=1
set BUILD_PYTHON_LAYER=1
set USE_INTEL_SPATIAL=1
set USE_GREENTEA=1
set USE_ISAAC=1
set RUN_TESTS=0
set RUN_INSTALL=1
set PYTHON_VERSION=3
call scripts\build_win.cmd

echo "clCaffe compile done"

注意事项:
编译过程中会报一次错,错误为找不到caffe/proto/caffe.pb.h

这个错误不是本机编译环境的问题,而是因为这个项目还不完善,在Windows*下编译项目的顺序有些问题。在Windows*下在编译pretune_convert.vcxproj的时候,这个caffe.pb.h还没有生成。

解决办法也很简单,直接再执行一遍build-clcaffe.cmd中下图的这部分脚本即可。

::设置编译参数,开始编译clCaffe
cd %~sdp0
cd caffe
set BUILD_PYTHON=1
set BUILD_PYTHON_LAYER=1
set USE_INTEL_SPATIAL=1
set USE_GREENTEA=1
set USE_ISAAC=1
set RUN_TESTS=0
set RUN_INSTALL=1
set PYTHON_VERSION=3
call scripts\build_win.cmd

最终编译结束了

接下来要把编译出的一些动态库拷贝到caffe\build目录下,具体请参考下面的编译脚本build-install.cmd。

if not exist "%~sdp0\caffe\build\install\" (
	echo do not find caffe build
)else (

	:: copy lib and include
	copy /y %~sdp0\dlfcn-win32\Release\dl.dll %~sdp0\caffe\build\install\bin
	copy /y %~sdp0\isaac\build\lib\Release\isaac.dll %~sdp0\caffe\build\install\bin
	copy /y %~sdp0\dlfcn-win32\Release\dl.dll %~sdp0\caffe\build\install\python\caffe
	copy /y %~sdp0\isaac\build\lib\Release\isaac.dll %~sdp0\caffe\build\install\python\caffe
	copy /y %~sdp0\dlfcn-win32\Release\dl.dll %~sdp0\caffe\build\tools\Release
	copy /y %~sdp0\isaac\build\lib\Release\isaac.dll %~sdp0\caffe\build\tools\Release

	copy /y %~sdp0\caffe\build\libraries\lib\boost_python-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_system-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_thread-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_filesystem-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_regex-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_chrono-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_date_time-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\boost_atomic-vc140-mt-1_61.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\glog.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\gflags.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\libprotobuf.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\caffehdf5_hl.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\caffehdf5.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\caffezlib.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\lmdb.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\leveldb.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\snappy_static.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\lib\libopenblas.dll.a %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\x64\vc14\lib\opencv_highgui310.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\x64\vc14\lib\opencv_videoio310.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\x64\vc14\lib\opencv_imgcodecs310.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\x64\vc14\lib\opencv_imgproc310.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\caffe\build\libraries\x64\vc14\lib\opencv_core310.lib %~sdp0\caffe\build\install\lib
	copy /y %~sdp0\isaac\build\lib\Release\isaac.lib %~sdp0\caffe\build\install\lib

	copy /y %OPENCL_LIBRARIES% %~sdp0\caffe\build\install\lib
	copy /y %PYTHON_LIBRARY% %~sdp0\caffe\build\install\lib

	xcopy %~sdp0\caffe\build\libraries\include %~sdp0\caffe\build\install\include /s /h /c /y
	move /y %~sdp0\caffe\build\install\include\boost-1_61\boost %~sdp0\caffe\build\install\include\boost
	mkdir %~sdp0\caffe\build\install\include\viennacl
	xcopy %~sdp0\caffe\viennacl-dev\viennacl %~sdp0\caffe\build\install\include\viennacl /s /h /c /y
	mkdir  %~sdp0\caffe\build\install\include\CL
	xcopy %~sdp0\caffe\viennacl-dev\CL %~sdp0\caffe\build\install\include\CL /s /h /c /y
	mkdir  %~sdp0\caffe\build\install\include\3rdparty
	xcopy %~sdp0\caffe\include\3rdparty %~sdp0\caffe\build\install\include\3rdparty /s /h /c /y

	echo "copy done"
)

运行

运行一下clCaffe项目自带的examples里的00-classification的代码来验证一下clCaffe是否能够正常运行,我们也可以由此看到clCaffe和BVLC caffe的运行流程上的一些不同

首先在C盘的根目录下建一个clcaffe_cache的目录,然后在这个clcaffe_cache的目录下建一个viennacl的子目录。

同时在Windows*的环境变量里加入VIENNACL_CACHE_PATH = c:\clcaffe_cache\viennacl

接下来看看电脑上的可使用的GPU设备的ID号,等下运行caffe的时候需要告诉caffe用哪个GPU设备。

打开Windows* 命令行控制台应用(command console),进入clcaffe-windows\caffe\build\tools\Release目录,执行

C:\work\clcaffe-windows\caffe\build\tools\Release>caffe.exe device_query

看一下输出,

可以看到我的电脑上有2个GPU设备,
Device id:0 是Intel HD Graphics 630核显
Device id:1 是CPU模拟的GPU设备

接着针对00-classification用的caffe模型,我们先生成一下caffe运行这个model所需要cache文件。(如果事先不生产cache文件,这个cache也会在caffe第一次运行caffe model的时候自动生成,但是会导致程序第一次运行时的运行时间过长)
打开Windows* 命令行控制台应用(command console),进入clcaffe-windows\caffe\build\tools\Release目录,执行

C:\work\clcaffe-windows\caffe\build\tools\Release>caffe.exe time -gpu 0 -phase TEST --model ..\..\..\models\bvlc_reference_caffenet\deploy.prototxt
  • 这个models\bvlc_reference_caffenet\deploy.prototxt就是用到的模型文件

Caffe会基于这个models来评估一个运行这个模型最快的opencl算法,并且把评估结果写到Cache文件里去。

准备工作终于结束了,下面跑一下00-classification例子吧

打开anaconda的命令行,进入clcaffe\caffe的examples目录,运行jupyter notebook

在打开的notebook中打开caffe自带的例子 00-calssification.ipynb

这是用一个训练好的Caffe模型来预测动物图片的例子,图片默认是使用Caffe项目里examples\image\cat.jpg。

先修改第2步中caffe的路径,将路径指向clcaffe

再修改第4步中的caffe运行模式,注释掉set_mode_cpu(),加上set_device(0),set_mode_gpu(),caffe接下来会把模型放到device id为0的GPU设备上。根据前面的caffe device_query的输出,id 0为本机的核显。

一路Shift+Enter运行下去,看到第8步predicted输出 

predicted class is: 281

第9步输出

output label: n02123045 tabby, tabby cat

预测结果是猫,说明clcaffe已经正确编译而且能运行了。

大功告成。

使用clCaffe的一些注意事项

  • clCaffe只支持Gen9及以上的Intel核显,即Intel Skylake架构及以后的微处理器的核显。
  • 生成caffe cache时建议预先用无权重系数模型来生产cache,不要在caffe第一次运行你自己的代码时on-the-fly的生成cache,容易造成GPU运行出错。(这个bug正在修复中)
  • Clcaffe在创建基于GPU的net模型时,这个net会基于set_device([GPU Device ID])传进去的那个GPU device ID创建。所以接下来这个net模型无法通过set_device()来切换另一个GPU硬件,如果想切换到另一个GPU上运行,必须通过set_device([GPU Device ID])设定一个新GPU Device,再重新定义一个新Net模型。

  • Windows*下clCaffe对python支持不好,python程序在退出时会异常。在Linux下无此问题。所以建议正式代码用C++来调用caffe,同时C++接口可以定义fp16的caffe模型,获得更高的性能。

Intel clCaffe核显带来的性能提升

接下来在我的两台PC机上分别运行一下基于CPU的BVLC caffe和基于GPU加速的clCaffe,看看在日常的学习生产硬件平台上(台式机和笔记本)运行caffe,核显加速能带来多少性能的提升。测试方法为在默认的Windows*10系统且安装了常用的办公软件及开发软件的环境下(不关闭任何默认打开的后台服务),利用前面用到的caffe自带的例子 00-classification,在代码的第11步,测试net.forward()的运行时间(如下图所示)。我们基于这个测试时间来做一个简单的性能对比。

基于Intel Core i5-7440HQ移动处理器的性能测试

CPU信息

CPU版本net.forward()运行时间

GPU信息

GPU版本net.forward()运行时间

性能提升
1560ms/309ms = 5.05倍

基于Intel Core i7-6700 桌面处理器的性能测试

CPU信息

CPU版本net.forward()运行时间

GPU信息

GPU版本net.forward()运行时间

性能提升
1370ms/262ms = 5.23倍

两个测试平台数据对比分析

  • 在Caffe的CPU实现上,Caffe模型的预测时间取决于CPU的核心数量和主频率
    net.forward()时间对比
    1560ms/1370ms = 1.14倍
    CPU频率对比
    3.4GHz/2.8GHz=1.21倍 
  • 在Caffe的GPU实现上,Caffe模型的预测时间主要取决于Net模型的复杂度和GPU的主频。
    net.forward()时间对比
    309ms/262ms = 1.18倍
    GPU频率对比
    1.15GHz/1GHz=1.15倍
  • 相同硬件平台上CPU实现和GPU实现对比,GPU版本的处理速度领先于CPU版本5倍以上
    net.forward()时间对比
    平台1:1560ms/309ms = 5.05倍
    平台2:1370ms/262ms = 5.23倍

结论

在人工智能领域,利用Intel核显GPU做硬件加速,在Caffe上做图像预测(Inference)时可以带来比纯CPU版本Caffe高达5倍以上的性能提升。这种使用场景特别适合使用Intel的低端桌面处理器,移动处理器,以及凌动处理器平台的IOT设备、Edge设备及家庭电脑上,在这种低功耗、低CPU性能的情况下可以利用Intel集成GPU大大提高这些硬件平台的AI预测速度。

后记

本文介绍的clCaffe并没有获得其最佳性能。要想让clCaffe获得最佳性能,我们还需要对模型进行优化(clCaffe采用的是模型融合),并采用FP16来进行推理。

模型融合(Model Fusion)意思是说在神经网络内,一些层可以合并在一起计算。通常情况下,我们可以将BatchNorm,Scale,Relu层合并进入Conv层。模型融合的好处是降低了数据读取的次数,因为在推理过程中除了运算,数据读写也占用了大量的时间。

FP16也称之为半精度浮点,一般浮点数为4字节(FP32),FP16顾名思义为2字节,大部分现代GPU中设计了FP16的运算单元, 相对FP32可以获得1.3~2倍数的性能提升。在clCaffe中可以创建Half类型的网络, 当这样的网络加载FP32的模型时,内部会自动转换成FP16的模型进行计算,速度可以进一步提升(但是,目前只能使用c/c++代码才能创建FP16网络)。

有关clCaffe的模型融合以及FP16推理相关的内容,将会在下一篇博文介绍。你也可以在这里https://github.com/liyuming1978/caffe_example找到更多的如何更好使用clCaffe的相关信息。

一门 VR:首款关于采用 MSI* 背包式 PC 的无线 VR 概念验证

$
0
0

Corey Warning 和 Will Lewis 是俄勒冈波特兰市的一家独立游戏工作室 Rose City Games* 的联合创始人。

Rose City Games 最近获得了一笔用于创建 VR 背包早期创新项目的开发补贴和设备预算。面临的挑战是想出一个只能在无线 VR 设置下实现的创意。在本文中,您将了解如何关于项目的设想和设计,我们所学到的知识,以及我们对未来的期望。以下视频详细介绍了这一项目。

项目灵感:一扇门

今年年初,我们的团队参加了俄勒冈波特兰市的 Resident Evil Escape Room我们是该游戏的忠实粉丝,通过全新的媒介体验这一游戏世界令人非常兴奋。此外,通过这一体验我们思考了还有哪些其他的体验能够以类似的方式进行跨越。

当时,我们尝试了所有能够动手实施的 VR 体验。当我们听说有机会尝试无线 VR 体验,我们就知道一定会发生有趣的事情。

目前,我们和从事各种 VR 项目的朋友在共同办公空间之外工作。WILD组员曾经尝试过将真实空间和 VR 相融合,所以我问 Gabe Paez是否记得当时项目进行过程中所遇到的具体挑战。他回答说是“门”,然后我决定继续创建“VR Escape Room”体验,其核心概念是穿过一扇门!

概述

项目范围是使用 MSI* One VR 背包创建 VR 应用概念验证。我们尝试创建一种只能使用硬件(具体来说是无线设置)实现的独特体验。

我们知道这一项目需要有一个装置,因此我们不考虑使用面向大众市场的产品。它可能是用于参加比如 GDC Alt.CtrlUnite*VR La等展览的有趣内容。

一门游戏概念

玩家将身处完全虚拟的空间中,与一扇实实在在的门进行交互。他们要背上 MSI VR One* 背包,并佩戴一个 HTC VIVE* 控制器和一个 VIVE 头盔。玩家必须在每一个关卡中完成一个简单的谜题或动作。完成后,玩家将能够打开门,跨过门槛进入下一关。这时他们面对是一个新的谜题或动作,游戏就是以这种方式向前推进。


图 1.一扇门概念验证设置

玩家可以随时打开门。但如果没有完成谜题或动作,另一端将显示同样的关卡/门。我们考虑使用 VIVE 跟踪来处理实际的门,这样我们就能轻松跟踪和校准玩家需要抓取的位置。


图 2.一扇门前视图


图 3.一扇门顶部视图

安装细节

  • 这扇门必须非常轻。
  • 支撑梁必须确保门不会倒向墙。
    • 底座位置的沙袋非常重要。
  • 应使用支架或类似工具,方便快速安装和拆卸,确保每次不会破坏整体性。
  • VIVE 灯塔的设置必须高于墙壁,以便捕捉整个游戏区域。
    • 需要质量优良的架子和较多沙袋。
  • 支撑梁/灯塔周围可能需要摆放比如豆袋椅,以确保人不会绊到这些东西。
    • 另外一个注意事项是必须安排人随时看着这一装置。


图 4.灯塔内的一门场地设置

我们的增建设施

  • 带有门把手和底座的移动门
  • MSI VR One 背包和用于开发的站外计算机
    • 其他必需的 DisplayPort-to-HDMI 线缆
    • 鼠标/键盘/显示器
    • OBS,用于捕捉视频
  • 2 个灯塔
    • 架子
    • 可调节抓爪,以将灯塔指向任意角度
    • 成对角线放置在门的两边
  • 1 个 VIVE 跟踪器
    • Gaffe 胶带,将跟踪器贴在门上,留出充电端口
    • 延伸线缆和充电线缆连接至跟踪器,以便充电
  • 2 个 VIVE 控制器
    • 我们用不到这两个控制器,但显示手部位置有利于录制视频
  • iPhone*,用于捕捉真实世界视频


图 5.门上贴有 VIVE*

这个项目是一次很好的 VR 开发培训。我们第一次使用 VIVE,并实施了其他物理增建设施来打造全新的交互式体验,是一次重要的学习过程。我认为我们遇到的大多数难题和所有 VR 开发人员所遇到的一样,但当然我们也给自己提出了许多独特的挑战,现在非常高兴有过这次体验。强烈建议 VR 开发人员在第一次独立开发项目之前,认真探讨以下主题,并学习我们的假设和过程。

首次使用 HTC VIVE*

我们经常用 VIVE 玩游戏,但却是第一次用它来开发项目。设置通用开发人员环境和 Unity* 插件并不费时间,但我们必须从战略上思考如何开发和测试更无缝地通过那个点。通常,在现场安排两个人可帮助我们节省大量的时间:一个人负责 Unity,另一个人移动控制器和跟踪器、重新调节灯塔、调节房间规模,就像人的第二双眼睛。


图 6.一门 VR 开发和测试

关于具体硬件以及需要使用物理道具的项目,我们反复进行了设计,以便灯塔能够跟踪到设备,甚至在连接显示器方面遇到了一些麻烦。由于 MSI VR One 背包只有一个 HDMI 输出和一个 DisplayPort 输入,我们必须借用(后来购买了)DisplayPort-to-HDMI 转换器,以便同时开发应用和使用 VIVE 头盔。幸运的是,这并没有过多地延缓开发进程,而且比最开始的解决方法还要好,我们之前将 HDMI 输出连接至已有的 HDMI 交换机,在显示器/设备环境和头盔之间切换。如果在开发过程中继续使用这种解决方案是非常不切实际的,一定会浪费大量时间。

我们在这次项目开发过程中获得了很多全新的体验,比如能够在家里使用 Unity 的协作功能远程工作,探索无线 VR 体验的精妙之处,以及熟悉以多快的速度启动 VR 项目。

预算

除了测试新设备和搭建物理增建设施,预算是我们不得不解决的另一个挑战。专项资金无法完全覆盖英特尔提供的设备推荐列表,因此我们只能挑选用于项目的最小数量的设备,然后考虑剩余的款项如何满足经验丰富的开发人员所投入的时间。幸运的是,由于我们与当地游戏开发人员社区的密切关系,一位有兴趣试验这种项目的朋友与我们合作开发这一项目。如果我们从头开始这个项目,我们一定提高预算,因为我们要在最低要求中考虑使用至少两台以上的跟踪器、转换器线缆、用于灯塔架顶端的可调节接头以及其他设备,以通过更完美的产品在紧迫的时间期限内完成这一项目。

位置和空间

从消费者的角度来看,我们知道对许多消费者来说,房间规模的 VR 是不现实的,而且在我们进行项目规划和实施的过程中,仍然遇到了一些问题。如果其他开发人员开发房间规模 VR,我们强烈建议提前购买卷尺,并确保有单独用于该项目的空间,保证开发的完整性。我们与其他 20 位当地的 VR 开发人员、美术师、游戏开发人员和网站设计师共用一个共同办公空间,因此每次开发会议结束之后需要将增建设施推至房间一侧,这样增加了整体的设置时间。的确我们练习了如何设置,也提高了我们对设备的熟悉程度,但同时也揭示了一个有趣的现象,那就是我们无法从家里进行这项工作!

独特的增建设施

由于项目涉及道具(一扇全尺寸树立的门),我们必须充分考虑如何移动、保存以及它对灯塔的遮挡。我们考虑进入原型之后的下一阶段时,更多的问题浮现出来。考虑如何让这一项目在今后继续发挥作用,比如用作技术演示、节日/博物馆装置或 resume piece,我们还必须考虑,除了我们自己和直接赞助方外,我们需要向更多人展示这一项目。这样出现了另一个需要考虑的因素:安全性。我们肯定会走捷径快速构建一个功能性原型,但考虑到改进和运输就绪性,我们绝对建议花费更多的时间和资源为不熟悉 VR 的人打造更加安全的体验。

 

因为我们构建过原型,因此能够记住抬脚不被绊倒,慢慢向前移动避免撞到门上,并且能够毫不费力地找到门把手。我们的成果看用作优秀的技术演示,但考虑到作为可供消费的公共产品或体验之前,我们肯定会用不同的方式处理门道具。为了让运输更方便,我们还用不同的方式建造门,以便在运输过程中拆卸。

展望未来

我们坚信,我们的项目能够作为技术演示,展示如何轻松、有趣、自由地使用 MSI VR One 背包。而且对于在这一过程中所学到的知识和最后的成果,我们感到非常自豪和高兴。因此我们想继续实施简单谜题、美术、画外音和可访问性特性,以便将该项目更完美地呈现出来。经过其他的测试和改进,我们想对原型货比三家、寻找与内容和 IP、VR 技术、交互装置相关的赞助商或展会,以便与广大受众一起分享这一项目!英特尔是此次合作的首选,我们希望在新一轮演示之后继续跟进。

非常感谢让我们参与其中!

代码示例 (Unity)

使用与门一般大小的外设时,关于灯塔和跟踪器设置 — 尤其是跟踪器,房间的设计必须恰到好处,我们将跟踪器粘在门上以随时测量方位,这样有利于我们分辨门是否关闭或打开。我们编写了一个简单的设置脚本,以定位门、门框和门支架/稳定器。

Setup Helper 是一个简单的工具,支持解决相对于 VIVE 跟踪器位置的门和门框的位置和旋转问题。Setup Helper 在 Editor 模式下运行,不必切换至 Play 模式就可进行更新,运行应用之后必须禁用该工具,以允许门在游戏中独立于门框旋转。可创建多个 Setup Helper 以定位需要与门相对的其他几何体,如房间墙壁、地板、房间装饰等,以避免潜在的面向视觉/碰撞的间隙或修剪。

Setup Helper 层级结构如上所示。以下应用于用蓝色凸显的区域,包括跟踪器(粘在门上)和门口。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class SetupHelper :MonoBehaviour {
	public bool setDoorFrameToTracker = false;
	public GameObject doorFrameGo;
	public Transform trackerTransform;
    public bool trackRotation = false;
	public Vector3 doorframeShift;//used to set the difference in placement to make it fit perfectly on the tracker position
	// Use this for initialization
	void Start () {

	}

	// Update is called once per frame
#if UNITY_EDITOR
	void Update () {
		if (setDoorFrameToTracker)
			SetDoorFrameToTracker();
	}
	void SetDoorFrameToTracker()
	{
		doorFrameGo.transform.position = trackerTransform.position + doorframeShift;
        if (trackRotation)
            doorFrameGo.transform.rotation = trackerTransform.parent.rotation;
	}
#endif

}

关于作者

Corey Warning 和 Will Lewis 是俄勒冈波特兰市的一家独立游戏工作室 Rose City Games* 的联合创始人。

探索专门用于英特尔® 架构的 Unity Technologies ML-Agents*

$
0
0

摘要

本文介绍了如何在仅限 CPU 的环境中安装并运行 Unity Technologies ML-Agents*。展示了如何:

  • 不使用 CUDA* 和 cuDNN* 的情况下,在 Windows* 上训练与运行 ML-Agents Balance Balls示例。
  • 在专为英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)优化的 Windows 上执行 TensorFlow* CMake 构建。
  • 不使用 CUDA 和 cuDNN 的情况下,从零创建一个简单的 Amazon Web Services*(AWS)Ubuntu* Amazon Machine Image* 环境,构建并在 AWS 上训练面向 Linux* 的“无头”版 Balance Balls

简介

Unity Technologies于 2017 年 9 月发布了测试版 机器学习代理*(ML-Agents*),并借助 3D 游戏引擎提供了激动人心的 强化学习简介。根据 Unity 博客的介绍,这款开源 SDK 能够为学术研究人员、对“机器人、自动驾驶汽车和其他工业应用的训练机制”感兴趣的行业研究人员和游戏开发人员带来显著的优势。

Unity ML-Agents SDK 将 TensorFlow* 用作使用近端策略优化(PPO)算法训练代理的机器学习框架。GitHub* 下载页面提供了若干示例项目、入门示例和关于安装与使用这款 SDK 的文档。

对于某些开发人员,设置与运行 ML-Agents 环境对 CUDA* 和 cuDNN* 的深度依赖是这款 SDK 的一个缺点。事实证明,我们不仅可以只在 CPU 上探索 ML-Agents,还可以在 Windows® 10 电脑上执行 TensorFlow 自定义构建,以添加面向英特尔® 架构的优化。

本文将向您展示如何:

  • 不使用 CUDA 和 cuDNN 的情况下,在 Windows 上训练与运行 ML-Agents Balance Balls(见图 1)示例。
  • 在面向英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)优化的 Windows* 上执行 TensorFlow CMake 构建。
  • 不使用 CUDA 和 cuDNN 的情况下,从零创建一个简单的 Amazon Web Services*(AWS)Ubuntu* Amazon Machine Image*(AMI)环境,构建并在 AWS 上训练面向 Linux* 的“无头”版 Balance Balls


图 1.在 Unity* 软件上运行的经过训练的 Balance Balls 模型。

 

目标受众

本文面向接触过 TensorFlow、Unity 软件、Python*、AWS 和机器学习概念的开发人员。

系统配置

在准备本文的过程中使用了以下系统配置:

Windows 工作站

  • 英特尔® 至强® 处理器 E3-1240 v5
  • Microsoft Windows 10,1709 版本

Linux 服务器(训练)

  • 英特尔® 至强® 铂金 8180 处理器 @ 2.50 GHz
  • Ubuntu 服务器 16.04 LTS

AWS 云(训练)

  • 英特尔® 至强® 处理器
  • Ubuntu 服务器 16.04 LTS AMI

在“在云中训练 ML-Agents”部分,我们使用了免费的 Ubuntu 服务器 16.04 AMI 套餐。

安装通用 Windows 组件

本部分描述了通用软件组件的安装步骤,该组件是设置与运行 ML-Agents 环境所必需的。Unity ML-Agents 文档包含了安装与设置流程,用户可以通过链接访问 CUDA 和 cuDNN 安装指导网页。如果您的系统已经配置了兼容 CUDA的图形处理单元(GPU)卡,并且您不介意付出额外的努力,这样也可以,但不对此做要求。无论采用何种方式,我们建议您在进行下一步前查看 Unity ML-Agents 文档。 

安装通用软件组件需要 3 个步骤:

  1. 从软件包(参见此处)中下载并安装 Unity 2017.1 或更高版本。
  2. GitHub下载 ML-Agents SDK。解压文件并将它们移动到选定的项目文件夹(如 C:\ml-agents)。
  3. 点击此处,下载并安装面向 Windows 的 Python 3.6 版本的 Anaconda* 分发版。

安装预构建 TensorFlow*

本部分遵循了在仅限 CPU 支持的 Windows 上安装 TensorFlow 的指南。根据 TensorFlow 网站,“该 TensorFlow 版本通常更容易安装(一般需要 5 到 10 分钟),因此,即便您配备了 NVIDIA* GPU,我们建议您首先安装该版本。”执行以下步骤,在您的 Windows 10 系统上安装预构建 TensorFlow:

  1. 在开始菜单中,单击 Anaconda Prompt图标(见图 2),以打开新的终端。


    图 2. Windows* 开始菜单。

  2. 在提示符后输入以下命令:

    > conda create -n tensorflow-cpu python=3.5
    > activate tensorflow-cpu
    > pip install --ignore-installed --upgrade tensorflow

  3. 按照 TensorFlow 文档的说明,打开 Python 并输入以下命令,以确保正确执行安装:

    > python
    >>> import tensorflow as tf
    >>> hello = tf.constant('Hello')
    >>> sess = tf.Session()
    >>> print (sess.run(hello))

  4. 如果所有步骤都正确,“Hello”应在终端打印,如图 3 所示。


    图 3.Python* 测试输出。

    您可能会注意到类似图 3 所示的一条消息,即“您的 CPU 支持指令:该 TensorFlow 二进制未经过编译,无法使用 AVX AVX2(“Your CPU supports instructions that this TensorFlow binary was not compiled to use:AVX AVX2”)。该消息可能会因系统中英特尔® 处理器的不同而有所差异;它表示如果您从源代码构建 TensorFlow,它可以更快速地在电脑上运行,我们将在下一部分展开讨论。

  5. 收到提示后按下 CTRL+Z,以关闭 Python。
     
  6. 导航至之前下载的 ML-Agents 存储库的 python 子目录,然后运行以下命令,以安装所需的其他关联组件:

    > pip install.

  7. 请参阅《Balance Ball 示例入门》教程的构建 Unity 环境章节,以完成对 ML-Agents 教程的学习。

从源代码安装 TensorFlow

本部分描述了如何在 Windows 10 系统上构建一个优化版 TensorFlow。

TensorFlow 网站指出“我们不会正式支持在 Windows 上构建 TensorFlow;但是,如果您不介意在 Windows 或 TensorFlow CMake 构建中使用试验性很强的 Bazel,可以尝试在 Windows 上构建 TensorFlow。”但是,不要因为这则声明而气馁。在本部分,我们提供了关于如何在 Windows 系统上执行 CMake 构建的说明。

以下 TensorFlow 构建指南是对 GitHub上显示的分步 Windows 构建说明的补充。建议您在继续操作之前,查看 GitHub 文档,以全面了解构建过程。 

  1. 安装 Microsoft Visual Studio* 2015。请务必查看图 4 显示的编程选项。


    图 4. Visual Studio* 编程选项。

  2. 此处下载与安装 Git。接受面向安装的所有默认设置。
     
  3. 此处下载与提取 swigwin。将文件夹改为 C:\swigwin-3.0.12(请注意,您系统上的版本号可能有所不同)。
     
  4. 此处下载与安装 CMake 版本 3.6。在安装过程中,请务必检查 面向所有用户将 CMake 添加至系统路径(Add CMake to the system path for all users)选项。  
  5. 在开始菜单中,单击 Anaconda Prompt图标(见图 2),以打开新的终端。在提示符后输入以下命令:

    > conda create -n tensorflow-custom36 python=3.6
    > activate tensorflow-custom36

  6. 运行以下命令,以设置环境:

    > "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"

    (注:如果未发现 vcvarsall.bat,尝试此处提供的以下指令。)
     
  7. 复制 TensorFlow 存储库并为您的构建创建一个工作目录:

    cd /
    > git clone https://github.com/tensorflow/tensorflow.git
    > cd tensorflow\tensorflow\contrib\cmake
    > mkdir build
    > cd build

  8. 输入下列命令(注:请务必检查系统中的下列路径和库版本,因为它们可能会有所不同):

    > cmake ..-A x64 -DCMAKE_BUILD_TYPE=Release ^
    -DSWIG_EXECUTABLE=C:\swigwin-3.0.12/swig.exe ^
    -DPYTHON_EXECUTABLE=C:/Users/%USERNAME%/Anaconda3/python.exe ^
    -DPYTHON_LIBRARIES=C:/Users/%USERNAME%/Anaconda3/libs/python36.lib ^
    -Dtensorflow_WIN_CPU_SIMD_OPTIONS=/arch:AVX2

  9. 构建 pip 程序包,它将被创建为 .\tf_python\dist 目录中的 .whl 文件(如 C:\tensorflow\tensorflow\contrib\cmake\build\tf_python\dist\tensorflow-1.4.0-cp36-cp36m-win_amd64.whl)。

    > C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild /p:Configuration=Release tf_python_build_pip_package.vcxproj

    (注:请务必检查系统中的 MSBuild 路径,因为它们可能会有所不同。)
     
  10. 输入以下命令,安装新创建的 TensorFlow 构建:

    pip install C:\tensorflow\tensorflow\contrib\cmake\build\tf_python\dist\tensorflow-1.4.0-cp36-cp36m-win_amd64.whl

  11. 按照 TensorFlow 文档的说明,打开 Python 并输入以下命令,以确保正确执行安装:

    > python
    >>> import tensorflow as tf
    >>> hello = tf.constant('Hello')
    >>> sess = tf.Session()
    >>> print (sess.run(hello))

  12. 如果所有步骤都正确,“Hello”应在终端打印。我们不会看到上文中出现的任何构建优化警告(见图 5)。


    图 5. Python* 测试输出。

  13. 收到提示后按下 CTRL+Z,以关闭 Python。
     
  14. 导航至之前下载的 ML-Agents 存储库的 python 子目录,然后运行以下命令,以安装所需的其他关联组件:

    > pip install .

  15. 请参阅《Balance Ball 示例入门》教程的构建 Unity 环境章节,以完成对 ML-Agents 教程的学习。

在云中训练 ML-Agents

ML-Agents 文档提供了名为《在 Amazon–Web Service 上进行训练》的指南,包括用于在 AWS 上设置 EC2 实例(以训练 ML-Agents)的说明。虽然该指南声明“您需要一个包含最新版 Nvidia* 驱动程序、CUDA8 和 cuDNN 的 EC2 实例”,但是基于云的训练可以采取更简单的方式,同时不产生 GPU 开销。

在本部分,我们执行以下步骤:

  • 创建一个 Ubuntu 服务器 16.04 AMI(免费套餐)。
  • 在 Windows 上安装先决组件应用,以便与云服务器进行交互。
  • 在 AMI 上安装 Python 和 TensorFlow。
  • 在 Windows 上构建无头 Linux 版 Balance Balls应用。
  • 导出 PPO.ipynb Jupyter 笔记本* 中的 Python 代码,使其在 Linux 环境中作为独立脚本运行。
  • python目录从 Windows 复制至 Linux AMI。
  • 在 AWS 上运行面向 ML-Agents Balance Balls应用的训练。
  1. 如果您没有账户,需要在 AWS 上创建一个账户。您可以按照本部分展示的步骤创建一个 AWS 免费套餐账户;但是,我们不会具体介绍如何创建账户与配置 AMI,因为该网站包含详细的操作信息。
  2. 创建一个 Ubuntu 服务器 16.04 AMI。图 6 显示了我们准备本文所使用的机器实例。


    图 6. Linux* 服务器 16.04 LTS Amazon Machine Image*。

  3. 在您的 Windows 工作站上安装 PuTTY* 和 WinSCP*。AWS 网站提供了关于如何安装这些组件、如何使用 PuTTY 从 Windows 连接至 Linux 实例以及如何使用 WinSCP 将文件传输至 Linux 实例的详细说明和链接。
     
  4. 使用 PuTTY 登录 Linux 服务器 AMI,然后输入以下命令,以安装 Python 和 TensorFlow:

    > sudo apt-get update
    > sudo apt-get install python3-pip python3-dev
    > pip3 install tensorflow
    > pip3 install image

    注:以下步骤假设您已完成 ML-Agents《Balance Ball 示例入门》教程的学习。如果您未完成该教程,在进行下一步之前,请务必输入完整的指令,并验证您能否在本地 Windows 工作站上成功训练与运行模型。
     
  5. 确保您的 Unity 软件安装包括 Linux 构建支持。您需要在安装时明确指定该选项,或者您可以通过运行 Unity Download Assistant 将它添加至现有的安装(如图 7 所示)。


    图 7.Unity* 软件 Linux* 构建支持。

  6. 在 Unity 软件中,打开 File – Build Settings并选择以下选项:
    • 目标平台:Linux
    • 架构:x86_64
    • 无头模式:已检查
  7. 设置如图 8 所示。


    图 8.面向无头 Linux 操作的 Unity* 软件构建设置。

  8. 点击 Build后,为应用创建一个独特的名称,并将其保存至存储库的 python文件夹内(见图 9)。在我们的示例中,我们将它命名为 Ball3DHeadless.x86_64,并在文章的后续部分沿用该名称。


    图 9.构建 Linux* 应用。

  9. 为了在 Linux AMI 上运行完整的训练流程,我们将导出 PPO.ipynb Jupyter笔记本中的 Python 代码,使其在 Linux 环境中作为独立脚本运行。为此,执行以下步骤:

    - 在开始菜单中,单击 Anaconda Prompt图标(图 2),以打开新的终端。
    - 导航至 python文件夹,然后在命令行输入 Jupyter笔记本。
    - 打开 PPO.ipynb笔记本,然后单击 File – Download As – Python (.py)。将在 Windows 电脑的 Downloads 文件夹内保存一个名为“ppo.py”的新文件。
    - 将文件名改为“ppo-test.py”,然后将它复制到 ML-Agents 存储库中的 python文件夹内。
    -在文本编辑器中打开 ppo-test.py,然后将 env_name变量改为“Ball3DHeadless”:
    - env_name = “Ball3DHeadless” # Name of the training environment file.
    - 保存 ppo-test.py,然后继续进行下一步操作。

  10. 面向 Linux 环境构建了应用,并生成了测试脚本后,使用 WinSCP 将 python文件夹从 ML-Agents 存储库复制到 Ubuntu AMI。(关于如何使用 WinSCP 将文件传输至 Linux 实例的详细信息,请参阅 AWS 网站。)
  11. 在 PuTTY 控制台,导航至 python文件夹并运行以下命令:

    > cd python
    > chmod +x Ball3DHeadless.x86_64
    > python3 ppo-test.py

    如果一切进展顺利,您会看到如图 10 所示的已启动训练流程。


    图 10.在 Amazon Web Services* Linux* 实例上运行的训练过程。

总结

根据图 10 显示的输出,我们需要注意每次保存模型后,时间(精确到秒)被输出至控制台。在本文中,代码被添加至 ppo-test.py脚本,以粗略测量模型保存之间的训练时间。

为了插入代码,我们对 Python 脚本进行了以下修改:

import numpy as np
import os
import tensorflow as tf
import time # New Code
.
.
.
trainer = Trainer(ppo_model, sess, info, is_continuous, use_observations, use_states)
timer_start = time.clock() # New Code
.
.
.
Save_model(sess, model_path=model_path, steps=steps, saver=saver)
print(“ %s seconds “ % (time.clock() – timer_start)) # New Code
timer_start = time.clock() # New Code
.
.
.

通过使用非正式的性能指标,我们发现预构建 TensorFlow GPU 二进制和 Windows 工作站上仅限 CPU 的预构建二进制在平均训练时间方面的差异十分微小。相比 Windows 工作站上仅限 CPU 的预构建二进制,仅限 CPU 的自定义 TensorFlow 构建的训练速度领先了约 19%。在云内执行训练时,相比 Windows 上的自定义 TensorFlow 构建,AWS Ubuntu Server AMI 的执行速度提升了约 29%。

Viewing all 154 articles
Browse latest View live