nextjournal

2022-02-14T08:26:55.944249Z

Can it be that Clojure "fn" are never cached by Clerk? So in a code of:

(defn my-fn ....)

(def a-result (a-function-using-my-fn   my-fn ))
a-result would be re-evaluated every time, even though the body of my-fn is unchanged ?

2022-02-15T09:01:31.225199Z

yes. I was thinking about that. It is true that for me the use case of persistent caching was not super important, as none of the other notebooks has it.

2022-02-15T09:02:01.748639Z

for sure "only nippy" seems too restrictive to me.

2022-02-15T09:04:01.542499Z

I had the issue with "fns" and nippy before. functions cannot be serialized, as far as i remember.

2022-02-15T09:05:11.280229Z

just found this: https://github.com/redplanetlabs/nippy-serializable-fns

mkvlr 2022-02-15T09:21:08.970209Z

yep, I’m aware of that. So far not caching functions hasn’t been a problem since I’ve not yet run into code where the result is a function but evaluation of it takes a long time. Does that apply to your code above?

mkvlr 2022-02-15T09:22:43.385489Z

though that you’re seeing the unfreezable error makes me think it’s a different problem and can be solved by tweaking the allow list

2022-02-15T09:39:30.523449Z

Yes. The "result" above contains functions. It is the result of a ML model training, so takes long.

2022-02-15T09:40:52.375959Z

I see this a very frequent situation. It is idiomatic Clojure to pass maps around which contain fns, as fns are "first class".

2022-02-15T09:41:30.532969Z

> though that you’re seeing the unfreezable error makes me think it’s a different problem and can be solved by tweaking the allow list

2022-02-15T09:41:54.019819Z

why different ? Fns are "unfreezable", no ?

2022-02-15T09:42:23.269219Z

I uncommented the "error printing" in clerk to see them.

2022-02-15T09:42:48.046369Z

They are indeed hidden in current Clerk code.

2022-02-15T09:44:45.604619Z

I just noted that the training was done repeatedly (due to logging of the training process itself and slowness of notebook evaluation) It all worked, just slow due to repeated execution (as cache as not working for the fns inside my result)

mkvlr 2022-02-15T09:46:22.113669Z

yeah, we should certainly warn when we can’t freeze values

mkvlr 2022-02-15T09:47:09.265019Z

but you can see Clerk does not reevaluate these:

(ns test)

(defn my-fn [x]
  (prn :my-fn x)
  (:hello x))


(defn a-function-using-my-fn [f x]
  (prn :a-function-using-my-fn)
  (f x))

(def a-result
  (do
    (prn :a-result)
    (a-function-using-my-fn my-fn {:hello :world})))

2022-02-15T09:58:43.515629Z

Indeed, in your code it works. The issue is if a var contains a fn.

(defn my-fn [] nil)

(def b {:fn my-fn
        :y (do (println "slow") (Thread/sleep 10000) :a)})

2022-02-15T09:59:25.832179Z

In this b will be re-evaluated every time we do clerk/show!

2022-02-15T10:00:24.388709Z

So my initial comments was wrong.

2022-02-15T10:00:46.376249Z

This one fixes it for me: https://github.com/redplanetlabs/nippy-serializable-fns

2022-02-15T10:01:40.904989Z

So it allows indeed to serialize fn with nippy. I can make a PR to add it.

2022-02-15T10:03:45.929539Z

The "drawback" of freezing fns is the need of "identical code" in freeze and un-freeze. But "cleaning the cache" can guarantee this in clerk. Just to be documented, maybe.

mkvlr 2022-02-15T11:44:51.417509Z

looking into this now

2022-02-15T12:04:25.234329Z

I did this PR https://github.com/nextjournal/clerk/pull/81 but might need more testing. Inside a single JVM run it solves the freezing for fns it seems. It solved my issue, at least.

mkvlr 2022-02-15T12:05:11.320949Z

yep, saw it. I’m afraid this might also create a bunch of other problems

mkvlr 2022-02-15T12:07:25.695799Z

> All JVM instances that could end up deserializing a fn instance are required to be launched from the same precompiled jar. from https://tech.redplanetlabs.com/2020/01/06/serializing-and-deserializing-clojure-fns-with-nippy/

2022-02-15T12:19:13.382329Z

Not sure if this is a big problem in Clerk. It requires that "freeze" and "un-freeze" are called by the "same code" (at least the same code where the seralized function come from) In the typical situation of usage of the Clerk cache (I freeze "now", and un-freeze 2 minutes later) this is given. The Clerk cache is not typicaly used for long-term storage, is it ? (with a high chance of code changes in between) And "clean cache" will fix it in any case.

2022-02-15T12:19:46.595189Z

But indeed "my issue" could be solved by an in-memory cache as well. (so not using nippy at all)

mkvlr 2022-02-15T12:21:48.673949Z

yes the intention of Clerk’s cache is to be used for long term storage

mkvlr 2022-02-15T12:22:04.133919Z

which is why we persist to disk

2022-02-15T12:22:20.249189Z

We seem to have three options to "get a result" for a form: • re-compute • in-memory cache without nippy • current persistent nippy based cache all have pros and cons...

mkvlr 2022-02-15T12:22:22.920009Z

haven’t showcased or talked about this a lot but this is coming soon

2022-02-15T12:23:23.453789Z

interesting, was not on my radar so far,

2022-02-15T12:27:36.912989Z

"try nippy else re-compute" as current, rules Clerk out for analysis with long running operations. It seems to me that nippy has quite some more gaps in type coverage. "general serialisation of all types" is a hard problem But maybe we can start by logging , so we see when "try nippy" fails and re-compute was triggered.

2022-02-15T12:31:41.282599Z

I don't want to be forced to restrict my data, only to make Clerk caching work (or accept re-compute)

mkvlr 2022-02-15T12:44:07.157009Z

yep, understood and should be able to make this work

mkvlr 2022-02-15T12:44:19.985619Z

the in-memory cache doesn’t have the same restrictions as nippy ofc

mkvlr 2022-02-15T12:50:26.374469Z

I have a working fix

mkvlr 2022-02-15T12:50:37.184289Z

will clean it up and push after lunch

👍 1
2022-02-15T13:25:26.009699Z

I will test it, I have a good test case.

2022-02-15T13:26:14.529919Z

Maybe it can be even something to be later configurable per form: • cache in-memory • persistent cache • no cache with a per namespace default

mkvlr 2022-02-15T13:32:14.676239Z

https://github.com/nextjournal/clerk/pull/82

mkvlr 2022-02-15T13:32:21.841399Z

can you take this for a spin please?

2022-02-15T15:13:59.194519Z

#82 fixes my problem. (at least while in the same JVM process). So both #80 and #82 fix my issue with the fns, in 2 different ways. (while in the same JVM process) I cannot see a speed difference (in-memory cache vs nippy on-disk cache), not seems immidiate. #80 should make the cache work even across JVM runs, but I can not test this due to issue with freeze / unfreeze of tech datasets

mkvlr 2022-02-15T15:28:46.114189Z

@carsten.behring excellent, thanks! I’ll go with #82 for now, as that fixes an obvious bug. Feel free to play with the nippy fns approach in userspace, see https://github.com/nextjournal/clerk/pull/81#issuecomment-1040408571.

2022-02-16T19:40:39.046599Z

@mkvlr Maybe there is an other bug lurking... It seems to me that the caching only works "once", on the overnext evaluation it is evaluated again. It seem, that the memory-cache only works "once".

2022-02-16T19:45:19.346969Z

Not true... I need to test more. Seems to work. Is there a way I can make my own install of clerk ?? The "bb release:jar" is not working

mkvlr 2022-02-16T20:42:35.098729Z

can’t you use :local/root or :git/sha?

mkvlr 2022-02-16T20:44:15.834189Z

and clojure -T:build jar doens’t work?

2022-02-16T20:50:13.129379Z

No:

clojure -T:build jar
Cloning: git@github.com:nextjournal/cas
Downloading: org/slf4j/slf4j-nop/maven-metadata.xml from central
Error building classpath. Unable to clone /home/carsten/.gitlibs/_repos/ssh/github.com/nextjournal/cas
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

2022-02-16T20:50:35.017809Z

does not exist

mkvlr 2022-02-16T20:57:07.923299Z

it’s private

mkvlr 2022-02-16T20:57:37.675829Z

why do you need a jar as opposed to using it via :local/root or :git/sha?

2022-02-16T21:37:00.854219Z

yes, good idea 👍

2022-02-14T08:34:34.248519Z

The concreate code is this:

(def pipe-fn
  (ml/pipeline
   (mm/replace-missing [:BsmtCond :PoolQC] :value :NA)
   (mm/select-columns [:OverallQual :GarageCars :BsmtCond
                       :GrLivArea :1stFlrSF :2ndFlrSF :TotalBsmtSF :GarageArea :Neighborhood :YearBuilt
                       :SalePrice])
   (fn [ctx]
     (assoc ctx : (load-hp-data "train.csv.gz")))
   (mm/transform-one-hot [:OverallQual :GarageCars :Neighborhood :BsmtCond :PoolQC] :full)

   (mm/set-inference-target :SalePrice)
   {:metamorph/id :model}
   (mm/model {:model-type :smile.regression/gradient-tree-boost
              :max-depth 50
              :max-nodes 10
              :node-size 8
              :trees 2000})))



(def result
  (ml/evaluate-pipelines [pipe-fn] splits ml/rmse :loss))

2022-02-14T08:37:40.308199Z

pipe-fn is a function and it gets passed to evaluate-pipelines I see that evaluate-pipelines is re-run even without any change in the file. on nextjournal.clerk/show!

2022-02-14T09:57:29.598019Z

After enable the exceptions, I get indeed this: The type of pipe-fn is "un-freezable" and so it does not cache it.

:freeze-error #error {
 :cause "Unfreezable type: class scicloj.metamorph.core$pipeline$local_pipeline__41340"
 :data {:type scicloj.metamorph.core$pipeline$local_pipeline__41340, :as-str "#function[scicloj.metamorph.core/pipeline/local-pipeline--41340]"}
 :via
 [{:type clojure.lang.ExceptionInfo
   :message "Unfreezable type: class scicloj.metamorph.core$pipeline$local_pipeline__41340"
   :data {:type scicloj.metamorph.core$pipeline$local_pipeline__41340, :as-str "#function[scicloj.metamorph.core/pipeline/local-pipeline--41340]"}
   :at [taoensso.nippy$throw_unfreezable invokeStatic "nippy.clj" 1003]}]

2022-02-14T20:29:53.447819Z

Ok, I went more in detail and understood the reason for the issue. Clerk fails to cache any result which contains a "fn", so expression getting evaluated repeated. Simples show case:

(defn my-fn [] nil)

(def b {:fn my-fn
        :y (do (println "slow") (Thread/sleep 10000) :a)})

2022-02-14T20:31:11.557649Z

'b' does not get cached, so it evaluated on every call to show! even without code change,

mkvlr 2022-02-15T07:35:37.177029Z

it seems we can also improve this situation by falling back to an in-memory cache when the nippy cache (which is also persistent across JVM restarts) fails. This should make the caching work as long as you’re looking at the same notebook even if nippy cannot freeze & thaw it.