Skip to content

openblas-build crate #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Dec 19, 2020
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
39bee6d
Create openblas-build
termoshtt Sep 13, 2020
0dcd85d
Build option
termoshtt Sep 19, 2020
67f825a
Parse Makefile.conf generated by OpenBLAS Makefile.prebuild
termoshtt Sep 19, 2020
5c649ff
Replace fs_extra by walkdir
termoshtt Sep 20, 2020
1cd7755
s/Detail/MakeConf/g
termoshtt Sep 20, 2020
8cf53ac
Minor cleanup
termoshtt Sep 20, 2020
1954913
Add CI for openblas-build
termoshtt Sep 21, 2020
0fdb920
Revise tests
termoshtt Sep 21, 2020
d629085
Split into sub-crates
termoshtt Sep 21, 2020
f11c918
Update documents
termoshtt Sep 21, 2020
2c313df
Expose LinkInfo::parse
termoshtt Sep 21, 2020
0a383bf
Move CheckCall to openblas_build::
termoshtt Sep 21, 2020
51db03d
Get symbols for checking CBLAS, LAPACK, and LAPACKE
termoshtt Sep 21, 2020
5186921
List linked libraries
termoshtt Sep 22, 2020
8c1bc04
LibDetail::has_lib
termoshtt Sep 22, 2020
b79a202
Rename and documents
termoshtt Sep 22, 2020
7bc67f0
Add test for no_shared and no_lapacke
termoshtt Sep 22, 2020
d436454
Fix Default for Configure
termoshtt Sep 22, 2020
310a48a
Add error handling if fortran compile does not exists
termoshtt Oct 3, 2020
59bb690
Install gfortran on CI
termoshtt Oct 3, 2020
3258734
Restore openblas-src/source
termoshtt Nov 7, 2020
1a7543b
Output error log of `make` when failed
termoshtt Nov 14, 2020
d6f5bde
Split build targets
termoshtt Nov 14, 2020
5239e13
Add name for each test steps
termoshtt Nov 14, 2020
c2d2489
Use *.dylib for macOS
termoshtt Nov 14, 2020
07f2512
Drop macOS CI
termoshtt Dec 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/openblas-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: openblas-build

on:
push:
branches:
- master
pull_request: {}

jobs:
linux:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
test_target:
- build_no_lapacke
- build_no_shared
- build_openmp
container:
image: rust
env:
RUST_BACKTRACE: 1
steps:
- uses: actions/checkout@v1
with:
submodules: 'recursive'
- name: Install gfortran by apt
run: |
apt update
apt install -y gfortran
- name: Common minor tests
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path=openblas-build/Cargo.toml
- name: Build test
uses: actions-rs/cargo@v1
with:
command: test
args: ${{ matrix.test_target }} --manifest-path=openblas-build/Cargo.toml -- --ignored
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Rust
name: openblas-src

on:
push:
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[workspace]
members = [
"openblas-src",
"openblas-build",
]
1 change: 1 addition & 0 deletions openblas-build/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test_build/
9 changes: 9 additions & 0 deletions openblas-build/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "openblas-build"
version = "0.1.0"
authors = ["Toshiki Teramura <[email protected]>"]
edition = "2018"

[dependencies]
anyhow = "1.0"
walkdir = "2.3.1"
44 changes: 44 additions & 0 deletions openblas-build/Makefile.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
OSNAME=Linux
ARCH=x86_64
C_COMPILER=GCC
BINARY32=
BINARY64=1
CEXTRALIB=-L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../.. -lc
F_COMPILER=GFORTRAN
FC=gfortran
BU=_
FEXTRALIB=-L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../.. -lgfortran -lm -lquadmath -lm -lc
CORE=HASWELL
LIBCORE=haswell
NUM_CORES=12
HAVE_MMX=1
HAVE_SSE=1
HAVE_SSE2=1
HAVE_SSE3=1
HAVE_SSSE3=1
HAVE_SSE4_1=1
HAVE_SSE4_2=1
HAVE_AVX=1
HAVE_AVX2=1
HAVE_FMA3=1
MAKE += -j 12
SHGEMM_UNROLL_M=8
SHGEMM_UNROLL_N=4
SGEMM_UNROLL_M=8
SGEMM_UNROLL_N=4
DGEMM_UNROLL_M=4
DGEMM_UNROLL_N=8
QGEMM_UNROLL_M=2
QGEMM_UNROLL_N=2
CGEMM_UNROLL_M=8
CGEMM_UNROLL_N=2
ZGEMM_UNROLL_M=4
ZGEMM_UNROLL_N=2
XGEMM_UNROLL_M=1
XGEMM_UNROLL_N=1
CGEMM3M_UNROLL_M=8
CGEMM3M_UNROLL_N=4
ZGEMM3M_UNROLL_M=4
ZGEMM3M_UNROLL_N=4
XGEMM3M_UNROLL_M=2
XGEMM3M_UNROLL_N=2
44 changes: 44 additions & 0 deletions openblas-build/nofortran.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
OSNAME=Linux
ARCH=x86_64
C_COMPILER=GCC
BINARY32=
BINARY64=1
CEXTRALIB=-L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../.. -lc
F_COMPILER=GFORTRAN
FC=gfortran
BU=_
NOFORTRAN=1
CORE=HASWELL
LIBCORE=haswell
NUM_CORES=12
HAVE_MMX=1
HAVE_SSE=1
HAVE_SSE2=1
HAVE_SSE3=1
HAVE_SSSE3=1
HAVE_SSE4_1=1
HAVE_SSE4_2=1
HAVE_AVX=1
HAVE_AVX2=1
HAVE_FMA3=1
MAKE += -j 12
SHGEMM_UNROLL_M=8
SHGEMM_UNROLL_N=4
SGEMM_UNROLL_M=8
SGEMM_UNROLL_N=4
DGEMM_UNROLL_M=4
DGEMM_UNROLL_N=8
QGEMM_UNROLL_M=2
QGEMM_UNROLL_N=2
CGEMM_UNROLL_M=8
CGEMM_UNROLL_N=2
ZGEMM_UNROLL_M=4
ZGEMM_UNROLL_N=2
XGEMM_UNROLL_M=1
XGEMM_UNROLL_N=1
CGEMM3M_UNROLL_M=8
CGEMM3M_UNROLL_N=4
ZGEMM3M_UNROLL_M=4
ZGEMM3M_UNROLL_N=4
XGEMM3M_UNROLL_M=2
XGEMM3M_UNROLL_N=2
321 changes: 321 additions & 0 deletions openblas-build/src/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
//! Execute make of OpenBLAS, and its options
use super::*;
use std::{
fs,
os::unix::io::*,
path::*,
process::{Command, Stdio},
};
use walkdir::WalkDir;

fn openblas_source_dir() -> PathBuf {
// FIXME This cannot work with cargo publish/package
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("openblas-src/source");
if !path.join("Makefile").exists() {
panic!("OpenBLAS repository has not been cloned. Run `git submodule update --init`");
}
path
}

/// Interface for 32-bit interger (LP64) and 64-bit integer (ILP64)
#[derive(Debug, Clone, Copy)]
pub enum Interface {
LP64,
ILP64,
}

/// CPU list in [TargetList](https://github.com/xianyi/OpenBLAS/blob/v0.3.10/TargetList.txt)
#[derive(Debug, Clone, Copy)]
#[allow(non_camel_case_types)] // to use original identifiers
pub enum Target {
// X86/X86_64 Intel
P2,
KATMAI,
COPPERMINE,
NORTHWOOD,
PRESCOTT,
BANIAS,
YONAH,
CORE2,
PENRYN,
DUNNINGTON,
NEHALEM,
SANDYBRIDGE,
HASWELL,
SKYLAKEX,
ATOM,

// X86/X86_64 AMD
ATHLON,
OPTERON,
OPTERON_SSE3,
BARCELONA,
SHANGHAI,
ISTANBUL,
BOBCAT,
BULLDOZER,
PILEDRIVER,
STEAMROLLER,
EXCAVATOR,
ZEN,

// X86/X86_64 generic
SSE_GENERIC,
VIAC3,
NANO,

// Power
POWER4,
POWER5,
POWER6,
POWER7,
POWER8,
POWER9,
PPCG4,
PPC970,
PPC970MP,
PPC440,
PPC440FP2,
CELL,

// MIPS
P5600,
MIPS1004K,
MIPS24K,

// MIPS64
SICORTEX,
LOONGSON3A,
LOONGSON3B,
I6400,
P6600,
I6500,

// IA64
ITANIUM2,

// Sparc
SPARC,
SPARCV7,

// ARM
CORTEXA15,
CORTEXA9,
ARMV7,
ARMV6,
ARMV5,

// ARM64
ARMV8,
CORTEXA53,
CORTEXA57,
CORTEXA72,
CORTEXA73,
NEOVERSEN1,
EMAG8180,
FALKOR,
THUNDERX,
THUNDERX2T99,
TSV110,

// System Z
ZARCH_GENERIC,
Z13,
Z14,
}

/// make option generator
#[derive(Debug, Clone)]
pub struct Configure {
pub no_static: bool,
pub no_shared: bool,
pub no_cblas: bool,
pub no_lapack: bool,
pub no_lapacke: bool,
pub use_thread: bool,
pub use_openmp: bool,
pub dynamic_arch: bool,
pub interface: Interface,
pub target: Option<Target>,
}

impl Default for Configure {
fn default() -> Self {
Configure {
no_static: false,
no_shared: false,
no_cblas: false,
no_lapack: false,
no_lapacke: false,
use_thread: false,
use_openmp: false,
dynamic_arch: false,
interface: Interface::LP64,
target: None,
}
}
}

/// Deliverables of `make` command
pub struct Deliverables {
/// None if `no_static`
pub static_lib: Option<LibInspect>,
/// None if `no_shared`
pub shared_lib: Option<LibInspect>,
/// Inspection what `make` command really show.
pub make_conf: MakeConf,
}

impl Configure {
fn make_args(&self) -> Vec<String> {
let mut args = Vec::new();
if self.no_static {
args.push("NO_STATIC=1".into())
}
if self.no_shared {
args.push("NO_SHARED=1".into())
}
if self.no_cblas {
args.push("NO_CBLAS=1".into())
}
if self.no_lapack {
args.push("NO_LAPACK=1".into())
}
if self.no_lapacke {
args.push("NO_LAPACKE=1".into())
}
if self.use_thread {
args.push("USE_THREAD=1".into())
}
if self.use_openmp {
args.push("USE_OPENMP=1".into())
}
if matches!(self.interface, Interface::ILP64) {
args.push("INTERFACE64=1".into())
}
if let Some(target) = self.target.as_ref() {
args.push(format!("TARGET={:?}", target))
}
args
}

/// Shared or static library will be created
/// at `out_dir/libopenblas.so` or `out_dir/libopenblas.a`
pub fn build<P: AsRef<Path>>(self, out_dir: P) -> Result<Deliverables> {
let out_dir = out_dir.as_ref();
if !out_dir.exists() {
fs::create_dir_all(out_dir)?;
}
let root = openblas_source_dir();
for entry in WalkDir::new(&root) {
let entry = entry.unwrap();
let dest = out_dir.join(entry.path().strip_prefix(&root)?);
if dest.exists() {
// Do not overwrite
// Cache of previous build should be cleaned by `cargo clean`
continue;
}
if entry.file_type().is_dir() {
fs::create_dir(&dest)?;
}
if entry.file_type().is_file() {
fs::copy(entry.path(), &dest)?;
}
}

let out = fs::File::create(out_dir.join("out.log")).expect("Cannot create log file");
let err = fs::File::create(out_dir.join("err.log")).expect("Cannot create log file");

// Run `make` as an subprocess
//
// - This will automatically run in parallel without `-j` flag
// - The `make` of OpenBLAS outputs 30k lines,
// which will be redirected into `out.log` and `err.log`.
match Command::new("make")
.current_dir(out_dir)
.stdout(unsafe { Stdio::from_raw_fd(out.into_raw_fd()) }) // this works only for unix
.stderr(unsafe { Stdio::from_raw_fd(err.into_raw_fd()) })
.args(&self.make_args())
.status()
{
Ok(status) => {
if !status.success() {
let err =
fs::read_to_string(out_dir.join("err.log")).expect("Cannot read log file");
bail!("`make` returns non-zero status {}:\n{}", status, err);
}
}
Err(error) => {
bail!(
"Cannot start subprocess ({}). Maybe `make` does not exist.",
error
);
}
}

let make_conf = MakeConf::new(out_dir.join("Makefile.conf"))?;

if !self.no_lapack && make_conf.no_fortran {
bail!("No Fortran Compiler found. It is needed for compiling LAPACK.");
}

Ok(Deliverables {
static_lib: if !self.no_static {
Some(LibInspect::new(out_dir.join("libopenblas.a")))
} else {
None
},
shared_lib: if !self.no_shared {
Some(LibInspect::new(if cfg!(target_os = "macos") {
out_dir.join("libopenblas.dylib")
} else {
out_dir.join("libopenblas.so")
}))
} else {
None
},
make_conf,
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[ignore]
#[test]
fn build_no_shared() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_build/build_no_shared");
let mut opt = Configure::default();
opt.no_shared = true;
let detail = opt.build(path).unwrap();
assert!(detail.shared_lib.is_none());
}

#[ignore]
#[test]
fn build_no_lapacke() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_build/build_no_lapacke");
let mut opt = Configure::default();
opt.no_lapacke = true;
let detail = opt.build(path).unwrap();
let shared_lib = detail.shared_lib.unwrap();
assert!(shared_lib.has_lapack());
assert!(!shared_lib.has_lapacke());
}

#[ignore]
#[test]
fn build_openmp() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_build/build_openmp");
let mut opt = Configure::default();
opt.use_openmp = true;
let detail = opt.build(path).unwrap();
assert!(detail.shared_lib.unwrap().has_lib("gomp"));
}
}
226 changes: 226 additions & 0 deletions openblas-build/src/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
//! Check make results
use super::*;
use anyhow::Result;
use std::{
collections::HashSet,
fs,
hash::Hash,
io::{self, BufRead},
path::*,
};

/// Parse compiler linker flags, `-L` and `-l`
///
/// - Search paths defined by `-L` will be removed if not exists,
/// and will be canonicalize
///
/// ```
/// use openblas_build::*;
/// let info = LinkFlags::parse("-L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/10.2.0/../../.. -lc");
/// assert_eq!(info.libs, vec!["c"]);
/// ```
#[derive(Debug, Clone, Default)]
pub struct LinkFlags {
/// Existing paths specified by `-L`
pub search_paths: Vec<PathBuf>,
/// Libraries specified by `-l`
pub libs: Vec<String>,
}

fn as_sorted_vec<T: Hash + Ord>(set: HashSet<T>) -> Vec<T> {
let mut v: Vec<_> = set.into_iter().collect();
v.sort();
v
}

impl LinkFlags {
pub fn parse(line: &str) -> Self {
let mut search_paths = HashSet::new();
let mut libs = HashSet::new();
for entry in line.split(" ") {
if entry.starts_with("-L") {
let path = PathBuf::from(entry.trim_start_matches("-L"));
if !path.exists() {
continue;
}
search_paths.insert(path.canonicalize().expect("Failed to canonicalize path"));
}
if entry.starts_with("-l") {
libs.insert(entry.trim_start_matches("-l").into());
}
}
LinkFlags {
search_paths: as_sorted_vec(search_paths),
libs: as_sorted_vec(libs),
}
}
}

/// Parse Makefile.conf which generated by OpenBLAS make system
#[derive(Debug, Clone, Default)]
pub struct MakeConf {
pub os_name: String,
pub no_fortran: bool,
pub c_extra_libs: LinkFlags,
pub f_extra_libs: LinkFlags,
}

impl MakeConf {
/// Parse from file
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut detail = MakeConf::default();
let f = fs::File::open(path)?;
let buf = io::BufReader::new(f);
for line in buf.lines() {
let line = line.unwrap();
if line.len() == 0 {
continue;
}
let entry: Vec<_> = line.split("=").collect();
if entry.len() != 2 {
continue;
}
match entry[0] {
"OSNAME" => detail.os_name = entry[1].into(),
"NOFORTRAN" => detail.no_fortran = true,
"CEXTRALIB" => detail.c_extra_libs = LinkFlags::parse(entry[1]),
"FEXTRALIB" => detail.f_extra_libs = LinkFlags::parse(entry[1]),
_ => continue,
}
}
Ok(detail)
}
}

/// Library inspection using binutils (`nm` and `objdump`) as external command
///
/// - Linked shared libraries using `objdump -p` external command.
/// - Global "T" symbols in the text (code) section of library using `nm -g` external command.
#[derive(Debug, Clone)]
pub struct LibInspect {
path: PathBuf,
pub libs: Vec<String>,
pub symbols: Vec<String>,
}

impl LibInspect {
/// Inspect library file
///
/// Be sure that `nm -g` and `objdump -p` are executed in this function
pub fn new<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref();
if !path.exists() {
panic!("File not found: {}", path.display());
}

let nm_out = Command::new("nm")
.arg("-g")
.arg(path)
.output()
.expect("nm cannot be started");

// assumes `nm` output like following:
//
// ```
// 0000000000909b30 T zupmtr_
// ```
let mut symbols: Vec<_> = nm_out
.stdout
.lines()
.flat_map(|line| {
let line = line.ok()?;
let entry: Vec<_> = line.trim().split(" ").collect();
if entry.len() == 3 && entry[1] == "T" {
Some(entry[2].into())
} else {
None
}
})
.collect();
symbols.sort(); // sort alphabetically

let mut libs: Vec<_> = Command::new("objdump")
.arg("-p")
.arg(path)
.output()
.expect("objdump cannot start")
.stdout
.lines()
.flat_map(|line| {
let line = line.ok()?;
if line.trim().starts_with("NEEDED") {
Some(line.trim().trim_start_matches("NEEDED").trim().into())
} else {
None
}
})
.collect();
libs.sort();

LibInspect {
path: path.into(),
libs,
symbols,
}
}

pub fn has_cblas(&self) -> bool {
for sym in &self.symbols {
if sym.starts_with("cblas_") {
return true;
}
}
return false;
}

pub fn has_lapack(&self) -> bool {
for sym in &self.symbols {
if sym == "dsyev_" {
return true;
}
}
return false;
}

pub fn has_lapacke(&self) -> bool {
for sym in &self.symbols {
if sym.starts_with("LAPACKE_") {
return true;
}
}
return false;
}

pub fn has_lib(&self, name: &str) -> bool {
for lib in &self.libs {
if let Some(stem) = lib.split(".").next() {
if stem == format!("lib{}", name) {
return true;
}
};
}
return false;
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn detail_from_makefile_conf() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Makefile.conf");
assert!(path.exists());
let detail = MakeConf::new(path).unwrap();
assert!(!detail.no_fortran);
}

#[test]
fn detail_from_nofortran_conf() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("nofortran.conf");
assert!(path.exists());
let detail = MakeConf::new(path).unwrap();
assert!(detail.no_fortran);
}
}
45 changes: 45 additions & 0 deletions openblas-build/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Helper crate for openblas-src/build.rs
//!
//! The `make` system of [OpenBLAS][OpenBLAS] has large number of inputs,
//! and detects environmental informations.
//!
//! Requirements
//! ------------
//!
//! This crate executes `make` as external command,
//! and inspects its deliverables using [GNU binutils][binutils] (`nm` and `objdump`).
//!
//! [binutils]: https://www.gnu.org/software/binutils/
//! [OpenBLAS]: https://github.com/xianyi/OpenBLAS
mod build;
mod check;
pub use build::*;
pub use check::*;

use anyhow::{bail, Result};
use std::process::Command;

pub(crate) trait CheckCall {
fn check_call(&mut self) -> Result<()>;
}

impl CheckCall for Command {
fn check_call(&mut self) -> Result<()> {
match self.status() {
Ok(status) => {
if !status.success() {
bail!(
"Subprocess returns with non-zero status: `{:?}` ({})",
self,
status
);
}
Ok(())
}
Err(error) => {
bail!("Subprocess execution failed: `{:?}` ({})", self, error);
}
}
}
}