MTJ-N + Intel MKL的配置和使用


前情提要

众所周知, 在python的世界, 矩阵库最好用的莫过于numpy, 类似于Matlab的语法, 让很多人从Matlab迁移过来毫无压力.

然而, 在Java的世界, 虽然以Spring为主的Web框架非常强大, 但是科学计算库的支持相比之下就相当感人. 倒并不是缺乏支持, 而是你很难找到一个足够好用效率又高支持又全的矩阵库.

当然实际上, 对于矩阵库的使用还是以效率为主, 所以今天要介绍的是Java世界中效率最高的矩阵库之一matrix-toolkits-java, 虽然作者已经不再维护, 但是性能经过实测依然吊打其他所有.

这个库性能高的主要原因在于它使用了Native库, 相比同样使用Native库的JBLAS, 这个库因为能够支持Intel MKL和Nvidia CUDA, 使其性能更高, 相关的Java矩阵库比较可以在Java Matrix Benchmark这里看到详细的比较.

正片开始

MTJ配置使用

MTJ在Maven中心仓库里, 所以如果使用Maven来管理依赖的话, 那么只要简单的添加即可:

<dependency>
    <groupId>io.github.andreas-solti.matrix-toolkits-java</groupId>
    <artifactId>mtj</artifactId>
    <version>1.0.7</version>
</dependency>

这里用的是另外一个Fork, 似乎是修正了原作者版本的一些问题, 不过没仔细看. 关于MTJ的文档可以参考这个: http://www.javadoc.io/doc/io.github.andreas-solti.matrix-toolkits-java/mtj/1.0.7.

使用这个库务必需要注意的是很多操作都是不复制而是在原矩阵上修改的, 例如:

public Matrix add(double alpha, Matrix B) {
    checkSize(B);

    if (alpha != 0)
        for (MatrixEntry e : B)
            add(e.row(), e.column(), alpha * e.get());

    return this;
}

public void add(int row, int column, double value) {
    set(row, column, value + get(row, column));
}

所以使用时如果要符合一般的使用习惯请务必注意:

/**
 * Simplify Some DenseMatrix Operation
 */
private static class DenseMatrixUtil{
    // return C=A+B
    public static DenseMatrix add(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.copy().add(B);
    }

    // return A=A+B
    public static DenseMatrix addi(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.add(B);
    }

    // return C=A-B
    public static DenseMatrix sub(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.copy().add(-1, B);
    }

    // return A=A-B
    public static DenseMatrix subi(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.add(-1, B);
    }

    // return C=s*A
    public static DenseMatrix mul(DenseMatrix A, double s){
        return (DenseMatrix)A.copy().scale(s);
    }

    // return A=s*A
    public static DenseMatrix muli(DenseMatrix A, double s){
        return (DenseMatrix)A.scale(s);
    }

    // return C=A*B
    private static DenseMatrix mmul(DenseMatrix A, DenseMatrix B){
        DenseMatrix C = new DenseMatrix(A.numRows(), B.numColumns()); // C.rows = A.rows, C.cols = B.cols
        return (DenseMatrix)A.mult(B, C);
    }

    // Compute \sum{alpha[i] * matrix[i]}
    private static DenseMatrix linear(double[] alphas, DenseMatrix[] matrixs){
        if(alphas.length != matrixs.length){
            throw new IllegalArgumentException("alpha's length should equal matrix's length");
        }

        DenseMatrix result = (DenseMatrix)new DenseMatrix(matrixs[0].numRows(), matrixs[0].numColumns()).zero();
        for(int i = 0; i < alphas.length; i++){
            result.add(alphas[i], matrixs[i]); // result = alpha * matrix + result
        }
        return result;
    }
}

与Intel MKL整合

Intel MKL全称是Intel® Math Kernel Library, 是Intel为其处理器设计的一套高度优化的Native矩阵与线性运算函数库, 相比OpenBLAS有着高得多的性能. 至于AMD处理器能不能使用, 这个我就不知道了, 但是我还是要喊出 AMD YES! (逃

Intel MKL安装

在Linux下, 只要直接从源里直接安装即可, 不同发行版不一样搜索一下即可, 以ArchLinux为例:

pacman -S intel-mkl

Arch的官方源里是没有MKL的, 如果需要的话, 请使用ArchlinuxCN或者Arch4Edu源.

在Windows下, 只能从官网注册开发者然后申请, 几天之后会给你发邮件然后提供下载连接下载Windows版的安装包.
这个安装包是包含devel部分的开发包, 事实上我们只需要redist部分即可, 为了方便使用我打包了2018.3.210的redist包(x86/x64). 事实上, Anaconda里面也是带Intel MKL Redist的, 完全可以从里面扒出来...

Intel MKL配置

在安装好Intel MKL以后还需要做两件事情:

  1. mkl\mkl_rt.dll软链接或者复制为libblas3.dllliblapack3.dll
  2. mklcompiler两个文件夹添加到系统的PATH环境变量中

这样, 才能够被MTJ-N所识别并使用.

如果不使用Intel MKL, MTJ似乎会自动使用OpenBLAS的实现, 其输出是下面这样的:

一月 14, 2019 11:31:15 上午 com.github.fommil.netlib.BLAS <clinit>
警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS
一月 14, 2019 11:31:15 上午 com.github.fommil.jni.JniLoader liberalLoad
信息: successfully loaded C:\Users\$USER\AppData\Local\Temp\jniloader7616017108689444760netlib-native_ref-win-x86_64.dll
一月 14, 2019 11:31:33 上午 com.github.fommil.netlib.LAPACK <clinit>
警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemLAPACK
一月 14, 2019 11:31:33 上午 com.github.fommil.jni.JniLoader load
信息: already loaded netlib-native_ref-win-x86_64.dll

性能虽然会比纯Java实现要好, 但是与MKL相比之下就要逊色很多了.

如果正确识别到MKL的话, 应该会类似的输出:

一月 14, 2019 11:38:25 上午 com.github.fommil.jni.JniLoader liberalLoad
信息: successfully loaded C:\Users\$USER\AppData\Local\Temp\jniloader3275505562389233726netlib-native_system-win-x86_64.dll
一月 14, 2019 11:38:26 上午 com.github.fommil.jni.JniLoader load
信息: already loaded netlib-native_system-win-x86_64.dll

微不足道的总结

如果要做科学计算方面的工作的话, 还是:

人生苦短, 快用Python (并不

给博客增加MathJax支持


MathJax是一个非常著名的用于在网页中显示公式的JS库, 主要是你用类似于>$\LaTeX$<的语法写下公式他就会自动帮你渲染非常方便. 比如:

$$ E=mc^2 $$

或者更加复杂一点:

$$ \normalsize \varepsilon=\sum_{i=1}^{n-1} \frac1{\Delta x}\int\limits_{x_i}^{x_{i+1}}\left\{\frac1{\Delta x}\big[ (x_{i+1}-x)y_i^\ast+(x-x_i)y_{i+1}^\ast\big]-f(x)\right\}^2dx $$

以前我也弄过这个, 不过用的是第三方的插件, 然后和Markdown混合渲染问题也很多, 最近发现直接在footer.php里添加脚本来加载就好了. 地址用的是一个国内的CDN, 有空再把地址配置到服务器上去把Orz

然后我把行内触发的标签从$$改成了>$$<, 因为两个美元符号还是蛮容易误触发的...

<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
        extensions: ["tex2jax.js"],
        jax: ["input/TeX", "output/HTML-CSS"],
        tex2jax: {
            inlineMath: [ ['>$','$<']],
            displayMath: [ ['$$','$$']],
            processEscapes: true
        },
        "HTML-CSS": { fonts: ["TeX"] }
    });
</script>
<script src='https://cdn.bootcss.com/mathjax/2.7.5/latest.js?config=TeX-MML-AM_CHTML' async></script>

顺便推荐一个MathTeX的参考网站参考书籍, 非常好使


如何将Markdown转换为MediaWiki


起因

虽然Markdown不是完美的标记语言, 或多或少有一些不足, 但是MediaWiki是真的难用! 所以如何将Markdown写好的文档转换为MediaWiki就变成了一个问题.

根据调研发现了Pandoc这样一个工具可以在任意两种文档之间进行转换, 具体支持哪些可以去官方的介绍界面去看, 当然我们只要知道它可以做到Markdown2MediaWiki的转换就好了.

这种转换经过测试, 基本上还可以, 就是代码块的转换上会有一点小问题, 不过这个会在后文给出正则表达式进行替换一下就好了.

使用方法

  1. 安装pandoc: https://pandoc.org/installing.html

    • WSL用户可以直接在WSL里使用包管理器安装源里的版本, 简单省事
  2. 打开终端(CMD)切换目录到源文件目录, 然后运行pandoc markdown.md -f markdown -t mediawiki -o mediawiki.wiki就可以完成文档的转换

    • Markdown的源文件是markdown.md, 输出文件是mediawiki.wiki, 当然可以自定义

一些问题

对代码块的处理有问题

pandonc默认是将```的代码块标记转换为HTML的<code>标签, 但是MediaWiki是不支持这个标签的, 因此需要将其转换为<pre>标签.

示例正则表达式如下:

Find: (</*)source([^>]*>)
Replace: \1pre\2

在Sublime下测试过没问题, 如果使用其正则表达式引擎特别是sed请自行测试修改.

总结

MediaWiki真的难用啊啊啊啊锕啊啊


利用VSCode构建简单的>$\LaTeX$<编辑器


起因

起因是因为要开始论文初稿的撰写, 然后需要使用>$\LaTeX$<. 虽然老板想让我在OverLeaf上写, 但是作为一个Vim党, 可能习惯了Vim的操作习惯以后就再也回不到纯文本的编辑上去了吧[笑]. 抛弃掉TexLive自带的TexWorks也是同理, VSCode的Vim插件总的来说还是挺好用的(虽然由于VSCode本身机制的问题会和一些插件不兼容, 比如acejump.

经过

现在是8102年了, 所以你需要的安装的是TexLive而不是CTex. 事实上, 对于中文现在TexLive也可以正常处理, 只要配置好字体就可以了1, 而且CTex也已经很久都没有更新过. 至于其他的发行版, 我只要求能用而且足够好用就可以了所以懒得圣战.

所以你需要的工具有:

安装方法非常简单:

  1. 安装VScode
  2. 安装TexLive2
  3. 安装Perl
  4. 打开终端运行tex看是否找到命令
  5. 安装VSCode的LaTeX Workshop插件
  6. 输入下列测试代码1保存:

    \documentclass[UTF8]{ctexart}
    \begin{document}
    你好,world!
    \end{document}
  7. 尝试运行左侧边栏的Build LaTeX Project - Recipe: latexmk/Recipe: pdflatex -> bibtex -> pdflatex*2应该都能够输出pdf
  8. 尝试运行左侧边栏的View LaTeX PDF - View in VSCode Tab/View in web browser就可以分别在VSCode的新标签页和浏览器打开了, 而且是会根据你编辑的内容实时刷新哟

其他的一些可能设置:

  • 输出到指定目录(比如./output)而不是根目录

    • 修改latex-workshop.latex.tools

          {
              "name": "latexmk",
              "command": "latexmk",
              "args": [
                  "-synctex=1",
                  "-interaction=nonstopmode",
                  "-file-line-error",
                  "-pdf",
                  "--outdir=output",
                  "%DOC%"
              ]
          },
    • 修改latex-workshop.latex.outputDir./output即可
  • 编译后清理掉中间文件

    • 修改latex-workshop.latex.tools

          {
              "name": "latexmk_build",
              "command": "latexmk",
              "args": [
                  "-synctex=1",
                  "-interaction=nonstopmode",
                  "-file-line-error",
                  "-pdf",
                  "--outdir=output",
                  "%DOC%"
              ]
          },
          {
              "name": "latexmk_clean",
              "command": "latexmk",
              "args": [
                  "-c",
                  "--outdir=output",
                  "%DOC%"
              ]
          },
    • 修改latex-workshop.latex.recipes

          {
              "name": "latexmk",
              "tools": [
                  "latexmk_build",
                  "latexmk_clean"
              ]
          },
  • 比如想要使用xelatex来排版

    • 打开VSCode的配置文件
    • latex-workshop.latex.recipes中加入如下内容:

      {
          "name": "xelatex",
          "tools": [
              "xelatex"
          ]
      },
    • latex-workshop.latex.tools中加入如下内容:

      {
          "name": "xelatex -> bibtex -> xelatex*2",
          "tools": [
              "xelatex",
              "bibtex",
              "xelatex",
              "xelatex"
          ]
      },
      {
          "name": "xelatex",
          "tools": [
              "xelatex",
          ]
      },
    • 重启VScode, 你就可以在Build LaTeX Project下看到新添加的Recipe了~

      xelatex -> bibtex -> xelatex*2用于处理需要bbl的情况, xelatex则用于纯>$\LaTeX$<环境下使用.
  • 在工作区排除掉LaTeX编译中间文件的显示

    • 加入如下配置到配置文件即可:

      "files.exclude": {
          "**/*.aux": true,
          "**/*.fdb_latexmk": true,
          "**/*.fls": true,
          "**/*.log": true,
          "**/*.synctex.gz": true,
          "**/*.bbl": true,
          "**/*.blg": true
      },
  • 补充一下自动clean的文件

    • latex-workshop.latex.clean.fileTypes中加入如下内容:

      "latex-workshop.latex.clean.fileTypes": [
          "*.aux",
          "*.bbl",
          "*.blg",
          "*.idx",
          "*.ind",
          "*.lof",
          "*.lot",
          "*.out",
          "*.toc",
          "*.acn",
          "*.acr",
          "*.alg",
          "*.glg",
          "*.glo",
          "*.gls",
          "*.ist",
          "*.fls",
          "*.log",
          "*.fdb_latexmk",
          "*.synctex.gz"
      ],
  • 其他的一些额外的有用的提示

    • 在VSCode的输出面板有LaTex WorkshopLaTeX Compiler两个输出选项, LaTex Workshop是LaTeX输出插件的日志, LaTeX Compiler是LaTeX编译器的输出日志
    • 如果没有pdf文件输出请检查编译器的日志, 查看VSCode是否找到了编译器是否正确编译
    • 如果有pdf文件, 但是pdf文件没有被打开请检查插件日志, 默认是打开主tex文件下同名pdf文件, 如果配置了输出目录需要修改配置latex-workshop.latex.outputDir

总结

VSCode不愧是一个全能型轻量级IDE, 通过各种插件的组合能够实现IDE的基本功能, 当然缺点主要是打开反应慢(和Sublime相比), 而且有些操作逻辑很迷(比如拖出标签页是保存文件???).

不过通过这一系列配置之后基本能实现实时预览和编写, 而且支持VScode完善的现代编辑器操作, 还是非常好评的w


如何通过反射从HttpServletRequest获取到HttpServletResponse对象(Tomcat限定


起因

项目会记录每次请求的进出便于调试和跟踪, 具体的实现是使用ServletRequestListenser的requestInitialized()和requestDestroyed()方法. 然后我们现在希望能够在请求出的时候同时记录到返回的状态码等信息, 这就需要获取到HttpServletResponse对象.

当然这个需求可能从一开始就是伪需求, 因为从Tomcat7开始就有提供AccessLog的记录, 使用Ngnix的访问记录也比在代码中去记录要好, 不过因为历史原因暂且就这样了╮( ̄▽ ̄)╭

发展

ServletRequestListener中的两个方法只提供了ServletRequestEvent参数, 然而你只能从这个类中获取到ServletRequest和ServletContext两个接口, 而在两个接口中是没有提供任何访问到Response的方法的, 因为这是由javax.serlvet包提供的接口.

然而事实上, 如果你使用的是Tomcat容器的话, 会发现这个接口在运行时的实现类是org.apache.catalina.connector.RequestFacade, 直接通过这个类我们依然获取不到Response, 但是它有一个类型为org.apache.catalina.connector.Request的属性, 而这个类他有一个getResponse的方法, 因此我们能通过这个类获取到Response对象.

1.png

事实上, org.apache.catalina.connector是位于tomcat根目录下lib目录下的catalina.jar, 出于部署方便的考虑, 我并不希望直接在工程中引入这个包, 所以就只能用万能的反射辣(x

Talk is Cheap. Show me the code.

    /**
     * 从Request中获取对应的Response的StatusCode
     * 采用了反射的方法, 可能只在限定版本的Tomcat中可用
     *
     * @param req Tomcat中的HTTPServletRequest对象
     * @return 提取出来的statusCode, 如果为-1, 说明提取出错
     */
    private Integer getStatusCode(HttpServletRequest req) {
        try{
            Field reqField = req.getClass().getDeclaredField("request");
            reqField.setAccessible(true);
            // org.apache.catalina.connector.Request
            Object reqObj = reqField.get(req);
            // org.apache.catalina.connector.Response
            Object resObj = reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj);
            return (Integer)resObj.getClass().getDeclaredMethod("getStatus").invoke(resObj);
        }catch(NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e){
            e.printStackTrace();
        }
        return -1;
    }

总结

其实这是一个很尬而且很难受的操作, 但是有的时候出于各种考虑又需要进行这样的操作. 这个时候反射就是我们的好朋友啦.

事实上, 从调用栈可以看到Tomcat在connector中的service()方法中就会创建该次请求的request与response, 并互相关联. 只不过javax.servlet.http包中的接口没有给出相应的方法也是很尴尬→_→

    public void service(Request req, Response res) throws Exception {
        org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request)req.getNote(1);
        org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response)res.getNote(1);
        if (request == null) {
            request = this.connector.createRequest();
            request.setCoyoteRequest(req);
            response = this.connector.createResponse();
            response.setCoyoteResponse(res);
            request.setResponse(response);
            response.setRequest(request);
            req.setNote(1, request);
            res.setNote(1, response);
            req.getParameters().setQueryStringEncoding(this.connector.getURIEncoding());
        }
        //...
    }